docs: veracity pass, screenshot enrichment, and screenshot pipeline split
Audit the user docs for accuracy against the code, enrich them with
component-level screenshots, and separate the doc-screenshot generation
path from the visual-regression gate so doc framing no longer churns test
baselines.
Veracity fixes (claims verified against code):
- connections: removed 3 screenshots that were from the unrelated mPWRD/nymea
WiFi-provisioning app and rewrote the TCP/IP section to match the real
Network transport flow (mDNS scan + manual IP:4403); replaced the BLE
"scan" image (it was the wifi-provision splash) with the real device list.
- nodes: online window is 2h (not 15min); binary online/offline, no "away" tier.
- map: markers are identity-colored node chips, not online-status colors.
- node-metrics & signal-meter: signal quality is preset-relative SNR, not
fixed thresholds.
- messages: max message length is 200 bytes (not 237/230).
- telemetry: CO2 bands aligned to Co2Severity (Good/Stuffy/Poor/Unsafe/Evacuate).
- translate: locale dirs use {lang}-r{REGION}.
New pages: Home Screen Widget, Help & In-App Docs (Chirpy on-device AI).
Screenshot enrichment + tighter framing: added IAQ scale, firmware verifying,
TAK local server, quick-chat dialog; cropped firmware states and connections
panes to component-level views instead of full-screen frames.
Pipeline split (new :docs-screenshots module, generate-only, not CI-gated):
holds doc-framed compositions so reframing a doc image never moves a regression
baseline; :screenshot-tests stays the gate. copyDocsScreenshots aggregates
both modules. Updated CI filter, governance advisories, dev docs, and the
testing-ci skill.
Translated docs re-sync from the English source via the scheduled Crowdin job.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
20
.github/workflows/docs-governance.yml
vendored
@@ -243,11 +243,11 @@ jobs:
|
||||
# Preview files changed
|
||||
preview_changed=$(echo "$changed" | grep -E 'Preview.*\.kt$' || true)
|
||||
|
||||
# Screenshot test files changed
|
||||
screenshot_tests_changed=$(echo "$changed" | grep -E '^screenshot-tests/src/screenshotTest/.*\.kt$' || true)
|
||||
# Screenshot test files changed (regression gate + generate-only docs module)
|
||||
screenshot_tests_changed=$(echo "$changed" | grep -E '^(screenshot-tests|docs-screenshots)/src/screenshotTest/.*\.kt$' || true)
|
||||
|
||||
# Reference images changed
|
||||
refs_changed=$(echo "$changed" | grep -E '^screenshot-tests/src/screenshotTestDebug/reference/.*\.png$' || true)
|
||||
# Reference images changed (either module)
|
||||
refs_changed=$(echo "$changed" | grep -E '^(screenshot-tests|docs-screenshots)/src/screenshotTestDebug/reference/.*\.png$' || true)
|
||||
|
||||
echo "ui_changed<<EOF" >> "$GITHUB_OUTPUT"
|
||||
echo "$ui_changed" >> "$GITHUB_OUTPUT"
|
||||
@@ -305,9 +305,9 @@ jobs:
|
||||
'',
|
||||
'**Adding previews checklist:**',
|
||||
'1. Create or update a `*Previews.kt` file in the same module with `@PreviewLightDark`',
|
||||
'2. Add `@Suppress("PreviewPublic")` if the preview is consumed by screenshot-tests',
|
||||
'3. Add corresponding `@PreviewTest` function in `screenshot-tests/src/screenshotTest/`',
|
||||
'4. Run `./gradlew :screenshot-tests:updateDebugScreenshotTest` to generate reference images',
|
||||
'2. Baseline the public preview in the module `detekt-baseline.xml` (or `@Suppress("PreviewPublic")`) if it is consumed cross-module by a screenshot wrapper',
|
||||
'3. Add a `@PreviewTest` wrapper: in `screenshot-tests/src/screenshotTest/` for a regression-gated component, or `docs-screenshots/src/screenshotTest/` for a doc-framed composition',
|
||||
'4. Run `./gradlew :screenshot-tests:updateDebugScreenshotTest` (and `:docs-screenshots:updateDebugScreenshotTest` for doc compositions) to generate reference images',
|
||||
'',
|
||||
'If this PR does **not** require preview updates (e.g., logic-only change, non-visual refactor), add the **`skip-preview-check`** label to dismiss.',
|
||||
].join('\n');
|
||||
@@ -357,9 +357,11 @@ jobs:
|
||||
'',
|
||||
'**How to update:**',
|
||||
'```bash',
|
||||
'./gradlew :screenshot-tests:updateDebugScreenshotTest',
|
||||
'./gradlew :screenshot-tests:updateDebugScreenshotTest # regression goldens',
|
||||
'./gradlew :docs-screenshots:updateDebugScreenshotTest # doc-framed compositions',
|
||||
'./gradlew :screenshot-tests:copyDocsScreenshots # refresh docs/assets from both',
|
||||
'```',
|
||||
'Then commit the updated reference PNGs.',
|
||||
'Then commit the updated reference PNGs (and any refreshed `docs/assets/screenshots/*.png`).',
|
||||
'',
|
||||
'If this change is intentionally preview-only (e.g., adding a preview that doesn\'t need a test yet), add the **`skip-preview-check`** label.',
|
||||
].join('\n');
|
||||
|
||||
1
.github/workflows/pull-request.yml
vendored
@@ -37,6 +37,7 @@ jobs:
|
||||
- 'core/**'
|
||||
- 'feature/**'
|
||||
- 'screenshot-tests/**'
|
||||
- 'docs-screenshots/**'
|
||||
# Shared build infrastructure
|
||||
- 'build-logic/**'
|
||||
- 'config/**'
|
||||
|
||||
@@ -62,9 +62,24 @@ Run these when relevant to map, provider, or flavor-specific behavior:
|
||||
./gradlew testFdroidDebug testGoogleDebug
|
||||
```
|
||||
|
||||
## 3b) Screenshot testing (two modules)
|
||||
|
||||
Compose Preview Screenshot Testing (AGP/layoutlib) is split into two modules — keep the distinction:
|
||||
|
||||
- **`:screenshot-tests`** — visual-regression **gate**. CI runs `:screenshot-tests:validateDebugScreenshotTest`. Holds atomic, dual-purpose components. Touching one of these previews is expected to move a gated baseline.
|
||||
- **`:docs-screenshots`** — **generate-only**, NOT validated in CI. Holds doc-framed compositions (crops/full screens tuned for the docs site). Reframe these freely; it never churns the regression gate.
|
||||
|
||||
```bash
|
||||
./gradlew :screenshot-tests:updateDebugScreenshotTest # regression goldens
|
||||
./gradlew :docs-screenshots:updateDebugScreenshotTest # doc-framed composition images
|
||||
./gradlew :screenshot-tests:copyDocsScreenshots # copy doc images from BOTH modules → docs/assets
|
||||
```
|
||||
|
||||
Rendering is **host-deterministic** (layoutlib): a local `update` produces references byte-identical to CI, so locally-recorded goldens pass `validate`. `copyDocsScreenshots` overwrites a stale committed `nodes_detail_local.png` each run — `git checkout` it. Public previews consumed cross-module by a wrapper need a `detekt-baseline.xml` entry (PreviewPublic). New screenshot? Pick the module by purpose; see `docs/assets/screenshots/README.md`.
|
||||
|
||||
## 4) CI Pipeline Architecture
|
||||
|
||||
CI is defined in `.github/workflows/reusable-check.yml` and structured as four parallel job groups:
|
||||
CI is defined in `.github/workflows/reusable-check.yml` and structured as parallel job groups:
|
||||
|
||||
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`):
|
||||
@@ -75,6 +90,7 @@ CI is defined in `.github/workflows/reusable-check.yml` and structured as four p
|
||||
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`) that builds desktop distributions via `createDistributable` (depends on `lint-check`).
|
||||
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).
|
||||
|
||||
### Runner Strategy (Three Tiers)
|
||||
- **`ubuntu-24.04-arm`** — Lightweight/utility jobs (status checks, labelers, triage, changelog, release metadata, stale, moderation). Benefits from ARM runners' shorter queue times.
|
||||
|
||||
@@ -42,8 +42,8 @@ changing linked areas; child directories change with their parent.
|
||||
|
||||
**Top-level areas requiring review**: `.agent_memory/`, `.agent_plans/`, `.agent_refs/`,
|
||||
`.github/`, `.specify/`, `androidApp/`, `app/`, `build-logic/`, `config/`, `core/`,
|
||||
`desktop/`, `desktopApp/`, `docs/`, `docs-site/`, `fastlane/`, `feature/`, `gradle/`,
|
||||
`ios/`, `iosApp/`, `offline-repository/`, `screenshot-tests/`, `scripts/`, `specs/`
|
||||
`desktop/`, `desktopApp/`, `docs/`, `docs-screenshots/`, `docs-site/`, `fastlane/`, `feature/`,
|
||||
`gradle/`, `ios/`, `iosApp/`, `offline-repository/`, `screenshot-tests/`, `scripts/`, `specs/`
|
||||
|
||||
## Development Commands
|
||||
|
||||
|
||||
62
docs-screenshots/build.gradle.kts
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import com.android.build.api.dsl.LibraryExtension
|
||||
import org.gradle.testretry.TestRetryTaskExtension
|
||||
|
||||
// Documentation screenshots — GENERATE-ONLY, intentionally NOT gated in CI.
|
||||
//
|
||||
// Unlike :screenshot-tests (a visual-regression gate run via :screenshot-tests:validateDebugScreenshotTest), this
|
||||
// module holds doc-framed composition previews whose framing is tuned for the documentation site. Its reference
|
||||
// images are regenerated on demand (./gradlew :docs-screenshots:updateDebugScreenshotTest) and consumed by
|
||||
// :screenshot-tests:copyDocsScreenshots; CI does NOT run validateDebugScreenshotTest here, so reframing a doc image
|
||||
// never churns the regression gate. Keep regression checks in :screenshot-tests, doc-only compositions here.
|
||||
plugins {
|
||||
alias(libs.plugins.meshtastic.android.library)
|
||||
alias(libs.plugins.meshtastic.android.library.compose)
|
||||
alias(libs.plugins.compose.screenshot)
|
||||
}
|
||||
|
||||
configure<LibraryExtension> {
|
||||
namespace = "org.meshtastic.screenshot.docs"
|
||||
|
||||
experimentalProperties["android.experimental.enableScreenshotTest"] = true
|
||||
|
||||
testOptions { screenshotTests { imageDifferenceThreshold = 0.0005f } }
|
||||
}
|
||||
|
||||
// CST screenshot tests use a custom runner incompatible with test-retry
|
||||
tasks.withType<Test>().configureEach {
|
||||
if (name.contains("ScreenshotTest", ignoreCase = true)) {
|
||||
extensions.configure<TestRetryTaskExtension> { maxRetries.set(0) }
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core:ui"))
|
||||
implementation(project(":core:resources"))
|
||||
implementation(project(":core:model"))
|
||||
implementation(project(":core:common"))
|
||||
implementation(project(":feature:connections"))
|
||||
implementation(project(":feature:firmware"))
|
||||
|
||||
implementation(libs.compose.multiplatform.foundation)
|
||||
implementation(libs.compose.multiplatform.material3)
|
||||
implementation(libs.compose.multiplatform.runtime)
|
||||
implementation(libs.compose.multiplatform.ui)
|
||||
|
||||
screenshotTestImplementation(libs.screenshot.validation.api)
|
||||
}
|
||||
2
docs-screenshots/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.screenshots.docs.feature
|
||||
|
||||
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
|
||||
// :screenshot-tests.
|
||||
|
||||
@PreviewTest
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
fun ScreenshotConnectionsBluetoothScan() {
|
||||
BluetoothScanPreview()
|
||||
}
|
||||
|
||||
@PreviewTest
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
fun ScreenshotEmptyStateContent() {
|
||||
EmptyStateContentPreview()
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.screenshots.feature
|
||||
package org.meshtastic.screenshots.docs.feature
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 27 KiB |
@@ -10,14 +10,27 @@ documentation pages. It is consumed by both:
|
||||
`DocImageWiringTest` (in `:feature:docs`) fails the build if a doc page references an image
|
||||
that is not present here.
|
||||
|
||||
## Two source modules
|
||||
|
||||
Doc screenshots come from Compose Preview Screenshot Testing references in **two** modules:
|
||||
|
||||
- **`:screenshot-tests`** — visual-regression gate (CI runs `:screenshot-tests:validateDebugScreenshotTest`).
|
||||
Holds atomic, dual-purpose components (signal/battery/hops info, list items, preference widgets,
|
||||
alerts) that are both regression-checked **and** used as doc images. Don't reframe these for docs.
|
||||
- **`:docs-screenshots`** — generate-only, **not** gated in CI. Holds doc-framed compositions whose
|
||||
framing is tuned for the docs site (e.g. the firmware status crops, the connections BLE-scan /
|
||||
empty-state crops). Reframing one here never churns the regression gate.
|
||||
|
||||
`copyDocsScreenshots` (in `:screenshot-tests`) aggregates the reference images from **both** modules.
|
||||
|
||||
## Updating Screenshots
|
||||
|
||||
Most screenshots are generated from the Compose Preview Screenshot Testing reference images
|
||||
in `screenshot-tests/src/screenshotTestDebug/reference/`. After changing a UI component:
|
||||
After changing a UI component, regenerate references for whichever module owns the wrapper, then copy:
|
||||
|
||||
```bash
|
||||
./gradlew :screenshot-tests:updateDebugScreenshotTest # regenerate reference images
|
||||
./gradlew :screenshot-tests:copyDocsScreenshots # refresh this directory
|
||||
./gradlew :screenshot-tests:updateDebugScreenshotTest # regression references
|
||||
./gradlew :docs-screenshots:updateDebugScreenshotTest # doc-framed composition references
|
||||
./gradlew :screenshot-tests:copyDocsScreenshots # refresh this directory from both
|
||||
```
|
||||
|
||||
`copyDocsScreenshots` copies **only** the light-mode reference images that have a semantic
|
||||
@@ -27,13 +40,15 @@ Commit the refreshed PNGs together with the reference-image changes.
|
||||
## Adding a Screenshot for a New Doc Page
|
||||
|
||||
1. Add (or reuse) a `Preview*`/`*Preview` composable with representative mock data in the
|
||||
feature module, and a `Screenshot*` wrapper in `screenshot-tests` (see
|
||||
`DiscoveryScreenshotTests.kt` for the pattern). If the component renders timestamps, give
|
||||
it a `timeTextOverride`-style parameter so renders stay deterministic across machines.
|
||||
2. Make sure the test class is covered by `screenshot-tests/docs-screenshots-manifest.txt`.
|
||||
feature module. Add a `Screenshot*` wrapper: in **`:docs-screenshots`** if it's a doc-framed
|
||||
composition (full screen / doc-specific crop), or in **`:screenshot-tests`** if it's an atomic
|
||||
component you also want regression-gated. If the component renders timestamps, give it a
|
||||
`timeTextOverride`-style parameter so renders stay deterministic across machines.
|
||||
2. Make sure the test class is covered by a pattern in `screenshot-tests/docs-screenshots-manifest.txt`
|
||||
(the patterns are `**/{Class}Kt/...`, so they match in either module).
|
||||
3. Map the semantic name in `screenshot-tests/docs-screenshot-aliases.properties`:
|
||||
`{page-id}_{description}.png=Screenshot{Name}_Light_{hash}_0.png`
|
||||
4. Run the two Gradle tasks above and reference the image from the doc page.
|
||||
4. Run the relevant update task(s) + `copyDocsScreenshots`, then reference the image from the doc page.
|
||||
|
||||
## Naming Convention
|
||||
|
||||
@@ -48,5 +63,7 @@ Examples: `onboarding_welcome.png`, `connections_bluetooth_scan.png`, `discovery
|
||||
- PNG format, light-mode only (dark variants live in the reference directory)
|
||||
- Name screenshots to match the docs page they appear in
|
||||
- Keep filenames lowercase with underscores
|
||||
- A few screenshots (`connections_wifi_*.png`) are manual captures with no CST source yet;
|
||||
they are hand-maintained until matching previews exist
|
||||
- Prefer CST-generated screenshots — they render real app composables, so they cannot drift from
|
||||
reality. Avoid hand-pasted captures: a stray screenshot from another app slipped in this way
|
||||
before (the old `connections_wifi_*.png` were from an unrelated WiFi-provisioning app, not
|
||||
Meshtastic). If a manual capture is unavoidable, it must be a genuine Meshtastic-Android screen.
|
||||
|
||||
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 32 KiB |
BIN
docs/assets/screenshots/firmware_verifying.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
docs/assets/screenshots/messages_edit_quick_chat.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
docs/assets/screenshots/node-metrics_iaq_scale.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
docs/assets/screenshots/tak_server_enabled.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
@@ -38,6 +38,8 @@ Keep the last 5–8 entries and trim older ones from the bottom.
|
||||
|
||||
**June 2026** — AIDL/`IMeshService` removed (#5586). The mesh service is now in-process only, driven entirely through `RadioController` — no cross-process binder, no `aidl` stubs.
|
||||
|
||||
**June 2026** — [Testing](developer/testing) — Split the screenshot pipeline: the new generate-only `:docs-screenshots` module holds doc-framed compositions, while `:screenshot-tests` stays the CI visual-regression gate — so reframing a doc image no longer churns a test baseline.
|
||||
|
||||
**June 2026** — New feature modules: `feature:discovery` (mesh network discovery, #5275) and `feature:car` (Android Auto / Car App Library, google flavor only, #5633).
|
||||
|
||||
**June 2026** — [Testing](developer/testing) — Added the `:baselineprofile` module (#5735): a Macrobenchmark cold-start journey generates a Baseline Profile for `:androidApp` to AOT-compile hot startup paths.
|
||||
|
||||
@@ -56,7 +56,8 @@ Meshtastic-Android/
|
||||
│ ├── testing/
|
||||
│ └── ui/
|
||||
├── baselineprofile/ # Baseline Profile generation for :androidApp
|
||||
├── screenshot-tests/ # Compose Preview screenshot tests
|
||||
├── screenshot-tests/ # Compose Preview screenshot tests (visual-regression gate)
|
||||
├── docs-screenshots/ # Doc-framed composition screenshots (generate-only, not CI-gated)
|
||||
├── build-logic/ # Convention plugins and build helpers
|
||||
│ ├── convention/
|
||||
│ └── flatpak/
|
||||
|
||||
@@ -56,14 +56,20 @@ Located in `commonTest` or `jvmTest` source sets.
|
||||
|
||||
### Screenshot Tests
|
||||
|
||||
Uses Android Gradle Plugin's native screenshot testing framework:
|
||||
Uses Android Gradle Plugin's native (layoutlib) screenshot testing framework, split across two modules:
|
||||
|
||||
- **`:screenshot-tests`** — the **visual-regression gate**. CI runs `validateDebugScreenshotTest` on it; reframing one of these baselines is a real diff to review. Holds atomic, dual-purpose components.
|
||||
- **`:docs-screenshots`** — **generate-only**, *not* validated in CI. Holds doc-framed compositions whose framing is tuned for the docs site, so reframing a doc image never churns the regression gate.
|
||||
|
||||
```bash
|
||||
./gradlew :screenshot-tests:updateDebugScreenshotTest # Record golden images
|
||||
./gradlew :screenshot-tests:validateDebugScreenshotTest # Compare against goldens
|
||||
./gradlew :screenshot-tests:copyDocsScreenshots # Copy reference images to docs pipeline
|
||||
./gradlew :screenshot-tests:updateDebugScreenshotTest # record regression goldens
|
||||
./gradlew :screenshot-tests:validateDebugScreenshotTest # compare against goldens (CI gate)
|
||||
./gradlew :docs-screenshots:updateDebugScreenshotTest # record doc-framed composition images
|
||||
./gradlew :screenshot-tests:copyDocsScreenshots # copy doc images from BOTH modules into docs/assets
|
||||
```
|
||||
|
||||
Rendering is host-deterministic here (layoutlib): a local `update` produces references byte-identical to CI, so locally-recorded goldens pass `validate`. See `docs/assets/screenshots/README.md` for which module a new screenshot belongs in.
|
||||
|
||||
### Baseline Profile / Startup Performance
|
||||
|
||||
The `:baselineprofile` module (#5735) generates a [Baseline Profile](https://developer.android.com/topic/performance/baselineprofiles/overview) for `:androidApp`, AOT-compiling the hot startup paths so ART doesn't pay the JIT cost on first launch. It targets the **google** flavor (the variant most users run).
|
||||
|
||||
@@ -19,6 +19,10 @@ Documentation for using the Meshtastic Android and Desktop app.
|
||||
Keep the last 5–8 entries and archive older ones by removing them.
|
||||
-->
|
||||
|
||||
**June 2026** — [Help & In-App Docs](user/help-and-docs) — New page covering the in-app documentation browser, search, and the on-device Chirpy AI assistant.
|
||||
|
||||
**June 2026** — [Home Screen Widget](user/widget) — New page covering the Android home screen widget that shows your connected radio's local stats at a glance.
|
||||
|
||||
**June 2026** — [Discovery](user/discovery) — Added the Local Mesh Discovery scanner: a dedicated mode that cycles your radio through LoRa presets, dwells on each to collect packets, and ranks which preset works best at your location.
|
||||
|
||||
**June 2026** — [Node Metrics](user/node-metrics) — Added Air Quality metrics (PM1.0, PM2.5, PM10, and CO₂ with severity color bands), a separate view from the BME680 IAQ reading.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
title: Connections
|
||||
parent: User Guide
|
||||
nav_order: 2
|
||||
last_updated: 2026-05-20
|
||||
last_updated: 2026-06-25
|
||||
description: Connect your phone or desktop to a Meshtastic radio via Bluetooth, USB, or TCP/IP.
|
||||
aliases:
|
||||
- bluetooth
|
||||
@@ -27,7 +27,7 @@ Bluetooth Low Energy is the default and most common connection method on Android
|
||||
4. Select your device from the list.
|
||||
5. Accept the Bluetooth pairing prompt if shown.
|
||||
|
||||

|
||||

|
||||
|
||||
You can filter devices by transport type using the filter chips at the top:
|
||||
|
||||
@@ -70,26 +70,20 @@ USB connections provide a wired alternative, useful for desktop or when Bluetoot
|
||||
|
||||
> ⚠️ **Note:** USB connections require OTG support on Android devices.
|
||||
|
||||
## TCP/IP (WiFi)
|
||||
## TCP/IP (Network)
|
||||
|
||||
Some Meshtastic radios support WiFi connectivity, allowing TCP-based connections.
|
||||
Some Meshtastic radios support WiFi/Ethernet connectivity, allowing TCP-based connections over your local network. Get the radio onto your network first — using the radio's own WiFi settings (via the firmware web interface or another connection) — then connect to it from the app.
|
||||
|
||||
### Configuration
|
||||
### Connecting over the Network
|
||||
|
||||
1. Connect your radio to a WiFi network via the radio's web interface or settings.
|
||||
2. In the app, go to **Connect → TCP**.
|
||||
3. Enter the radio's IP address and port (default: 4403).
|
||||
4. Tap **Connect**.
|
||||
1. Make sure the radio is on the same local network as your phone/desktop.
|
||||
2. On the Connect screen, select the **Network** transport filter.
|
||||
3. Choose the radio one of two ways:
|
||||
- **Scan Network Devices** — toggle this on to auto-discover radios that advertise themselves on the local network (mDNS / `_meshtastic._tcp`). Discovered devices appear in the list; tap one to connect.
|
||||
- **Add Network Device Manually** — enter the radio's IP address (or hostname) and port (default: `4403`).
|
||||
4. Previously-used network addresses are remembered under **Recent Network Devices** for quick reconnection (long-press to remove one).
|
||||
|
||||

|
||||
|
||||
When a device is found, it appears in the connection list:
|
||||
|
||||

|
||||
|
||||
A successful connection is confirmed with a status indicator:
|
||||
|
||||

|
||||
> 💡 **Tip:** Network discovery uses mDNS, which only works when both devices are on the same subnet. On Android 17+ the app needs the local-network permission for scanning; if discovery finds nothing, add the device manually by IP.
|
||||
|
||||
### When to Use TCP
|
||||
|
||||
|
||||
@@ -62,7 +62,11 @@ Before updating:
|
||||
|
||||
## Post-Update
|
||||
|
||||
After a successful update:
|
||||
After the firmware is written, the app verifies the update and waits for the device to come back online:
|
||||
|
||||

|
||||
|
||||
Once the update succeeds:
|
||||
- The radio will reboot automatically
|
||||
- Bluetooth connection will re-establish
|
||||
- Verify your settings are intact
|
||||
|
||||
49
docs/en/user/help-and-docs.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
title: Help & In-App Docs
|
||||
parent: User Guide
|
||||
nav_order: 21
|
||||
last_updated: 2026-06-25
|
||||
description: Browse this documentation inside the app, search it, and ask Chirpy — the on-device AI assistant — questions about Meshtastic.
|
||||
aliases:
|
||||
- help
|
||||
- docs-browser
|
||||
- chirpy
|
||||
- assistant
|
||||
---
|
||||
|
||||
# Help & In-App Docs
|
||||
|
||||
This same user documentation ships **inside the app**, so you can read it offline without leaving Meshtastic. Open it from **Settings → Help & Documentation**.
|
||||
|
||||
## Browsing
|
||||
|
||||
The docs browser lists every user-guide page. Tap a page to read it; images and cross-links work just like they do here.
|
||||
|
||||

|
||||
|
||||
### Search
|
||||
|
||||
Tap the search icon and type to filter pages by title and keywords — results update as you type.
|
||||
|
||||

|
||||
|
||||
A page open in the browser:
|
||||
|
||||

|
||||
|
||||
## Chirpy — the AI Assistant
|
||||
|
||||
**Chirpy** answers plain-language questions about Meshtastic using this bundled documentation as its source. Tap the Chirpy button in the docs browser, type a question, and it replies with an answer and links to the relevant pages.
|
||||
|
||||

|
||||
|
||||
> 🔒 **Privacy:** On supported Google-flavor devices, Chirpy runs **on-device** using Gemini Nano — your questions never leave your phone. A small model downloads on first use.
|
||||
|
||||
> ⚠️ **Note:** On F-Droid, Desktop, and iOS builds, Chirpy falls back to a **keyword search** over the documentation rather than a generative model. If your device doesn't support on-device AI, the assistant is hidden and you can still browse and search the docs normally.
|
||||
|
||||
## Related Topics
|
||||
|
||||
- [Translate the App](translate) — how these pages get localized into other languages
|
||||
- [App Functions](app-functions) — the separate system-AI integration (distinct from Chirpy)
|
||||
|
||||
---
|
||||
@@ -2,7 +2,7 @@
|
||||
title: Map & Waypoints
|
||||
parent: User Guide
|
||||
nav_order: 6
|
||||
last_updated: 2026-05-13
|
||||
last_updated: 2026-06-25
|
||||
description: View node positions on the map, create and share waypoints, and manage position sharing and privacy.
|
||||
aliases:
|
||||
- map
|
||||
@@ -24,13 +24,7 @@ The map displays:
|
||||
|
||||
### Node Markers
|
||||
|
||||
Node markers on the map indicate:
|
||||
| Color | Meaning |
|
||||
|-------|---------|
|
||||
| Green | Online (heard recently) |
|
||||
| Yellow | Away (heard within 2 hours) |
|
||||
| Gray | Offline (stale position) |
|
||||
| Blue | Your own node |
|
||||
Each node that reports a position is shown as a **node chip** marker displaying the node's short name. The chip is colored by the node's own identity color (a stable color derived from its node number) — the same chip used in the node list, so a node looks the same everywhere. Marker color does **not** encode online/offline status. When a node's position updates live, its marker briefly pulses. Nearby markers are clustered as you zoom out.
|
||||
|
||||
### Map Controls
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
title: Messages & Channels
|
||||
parent: User Guide
|
||||
nav_order: 3
|
||||
last_updated: 2026-06-11
|
||||
last_updated: 2026-06-25
|
||||
description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions.
|
||||
aliases:
|
||||
- channels
|
||||
@@ -79,7 +79,7 @@ When a message fails to deliver, the error indicator shows what went wrong:
|
||||
| No Interface | No radio interface available to send | Check that your radio is connected and the channel is configured. |
|
||||
| Max Retransmit | All retry attempts exhausted | The mesh path is unreliable. Try a different channel or wait for conditions to improve. |
|
||||
| No Channel | The destination channel doesn't exist | Verify both nodes share the same channel configuration. |
|
||||
| Too Large | Message exceeds maximum payload size | Shorten your message (max ~230 characters). |
|
||||
| Too Large | Message exceeds maximum payload size | Shorten your message (max ~200 characters). |
|
||||
| No Response | Node received message but didn't respond | The recipient's radio may be busy or in low-power sleep mode. |
|
||||
| Duty Cycle Limit | Regional airtime limit reached | Your radio has used its allowed transmit time. Wait for the duty cycle window to reset (typically 1 hour in EU regions). |
|
||||
| Bad Request | Malformed or invalid message | This usually indicates a software bug. Try restarting the app. |
|
||||
@@ -98,6 +98,10 @@ Pre-configured messages for rapid communication:
|
||||
|
||||

|
||||
|
||||
Each quick chat entry has a short **Name** (the button label), the **Message** it inserts, and an **Instantly send** toggle — when enabled, tapping the button sends the message immediately instead of placing it in the input field for editing:
|
||||
|
||||

|
||||
|
||||
The channel list shows each channel with its latest message preview.
|
||||
|
||||
### Searching Messages
|
||||
@@ -147,7 +151,7 @@ Messages are queued and transmitted based on priority:
|
||||
|
||||
### Message Limits
|
||||
|
||||
- **Maximum length:** 237 bytes (approximately 230 characters for ASCII text)
|
||||
- **Maximum length:** 200 bytes (approximately 200 characters for ASCII text)
|
||||
- **Rate limiting:** The mesh enforces airtime fairness; heavy message volume may be throttled
|
||||
- **Delivery:** Messages are retried automatically if no acknowledgment is received
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
title: Node Metrics
|
||||
parent: User Guide
|
||||
nav_order: 5
|
||||
last_updated: 2026-06-16
|
||||
last_updated: 2026-06-25
|
||||
description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history.
|
||||
aliases:
|
||||
- metrics
|
||||
@@ -45,6 +45,10 @@ Environmental sensor data (requires compatible hardware):
|
||||
|
||||
Environment metrics are charted over time for easy trend analysis — temperature, humidity, and pressure each get their own line chart with the measurement unit displayed on the Y axis.
|
||||
|
||||
The BME680 **IAQ (Indoor Air Quality)** index is a single 0–500+ value derived from gas resistance, shown against a color-coded scale from *Excellent* to *Dangerously Polluted*:
|
||||
|
||||

|
||||
|
||||
> 💡 **Tip:** Environment metrics require a sensor connected to the remote node. Not all nodes report environmental data. See [Telemetry & Sensors](telemetry-and-sensors) for a full list of supported sensors.
|
||||
|
||||
## Air Quality Metrics
|
||||
@@ -92,12 +96,16 @@ Radio signal quality information:
|
||||
|
||||
### Signal Quality Reference
|
||||
|
||||
| SNR Range | Quality |
|
||||
|-----------|---------|
|
||||
| > 10 dB | Excellent |
|
||||
| 0 to 10 dB | Good |
|
||||
| -10 to 0 dB | Fair |
|
||||
| < -10 dB | Poor |
|
||||
Signal quality is rated from **SNR relative to the active LoRa modem preset's demodulation floor**, not from fixed thresholds — a given SNR means different things on different presets (e.g. −15 dB is fine on LongSlow but unusable on ShortFast). RSSI is shown but is not part of the rating. Letting `limit` be the preset's SNR limit:
|
||||
|
||||
| Quality | Criteria |
|
||||
|---------|----------|
|
||||
| Good | SNR above the preset's limit |
|
||||
| Fair | within 5.5 dB below the limit |
|
||||
| Bad | within 7.5 dB below the limit |
|
||||
| None | more than 7.5 dB below the limit |
|
||||
|
||||
See [Understanding the Signal Meter](signal-meter) for the full explanation.
|
||||
|
||||
Local Stats from your connected radio are also shown in Signal Quality when available. These logs include noise floor, traffic counters, relay counters, online node counts, and radio uptime. The noise floor chart uses a dashed reference line at -85 dBm to help identify a busy RF environment. Use **Request** to ask the connected radio for a fresh Local Stats telemetry report, **Clear** to remove Local Stats logs for that node, and **Save** to export the visible Local Stats history as CSV.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
title: Nodes
|
||||
parent: User Guide
|
||||
nav_order: 4
|
||||
last_updated: 2026-06-02
|
||||
last_updated: 2026-06-25
|
||||
description: Browse, filter, and sort mesh nodes — view details, signal quality, roles, and quick actions.
|
||||
aliases:
|
||||
- node-list
|
||||
@@ -28,11 +28,12 @@ The node list shows every node your radio has heard, including:
|
||||
|
||||
| Badge | Meaning |
|
||||
|-------|---------|
|
||||
| 🟢 Online | Node heard within the last 15 minutes |
|
||||
| 🟡 Away | Node heard within the last 2 hours |
|
||||
| 🔴 Offline | Node not heard for over 2 hours |
|
||||
| 🟢 Online | Node heard within the last 2 hours |
|
||||
| ⚪ Offline | Node not heard for over 2 hours |
|
||||
| ⭐ Favorite | Node marked as favorite by the user |
|
||||
|
||||
A node is considered **online** if it was heard within the last 2 hours, and **offline** otherwise — there is no separate "away" tier.
|
||||
|
||||
### Node Roles
|
||||
|
||||
Nodes can be configured with different roles that affect their mesh behavior:
|
||||
@@ -101,7 +102,7 @@ Type in the search field to filter nodes by name or short name. The filter updat
|
||||
|
||||
| Filter | Description |
|
||||
|--------|-------------|
|
||||
| **Only online** | Show only nodes heard within the last 15 minutes |
|
||||
| **Only online** | Show only nodes heard within the last 2 hours |
|
||||
| **Only direct** | Show only nodes with direct (non-relayed) connections |
|
||||
| **Include unknown** | Show nodes that haven't sent user info yet |
|
||||
| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Repeater, Router Late, Client Base) |
|
||||
|
||||
@@ -55,6 +55,8 @@ After modifying settings, tap **Save** to write the configuration to your radio.
|
||||
|
||||
### Modem Presets
|
||||
|
||||
> 💡 **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.
|
||||
|
||||
| Preset | Range | Speed | SNR Limit | Best For |
|
||||
|--------|-------|-------|-----------|----------|
|
||||
| Short Turbo | ~1 km | 21.9 kbps | −5 dB | Dense urban with line-of-sight; data-heavy applications |
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
title: How the Meshtastic Signal Meter Works
|
||||
parent: User Guide
|
||||
nav_order: 15
|
||||
last_updated: 2026-05-13
|
||||
description: How the signal meter calculates quality from RSSI and SNR — LoRa spread spectrum, presets, and what the bars really mean.
|
||||
last_updated: 2026-06-25
|
||||
description: How the signal meter rates quality from SNR relative to the LoRa modem preset — spread spectrum, presets, and what the bars really mean.
|
||||
aliases:
|
||||
- signal
|
||||
- signal-meter
|
||||
@@ -13,7 +13,7 @@ aliases:
|
||||
|
||||
# How the Meshtastic Signal Meter Works
|
||||
|
||||
The Meshtastic signal meter — the familiar bars or status color in the app — is calculated very differently than the "bars" on a traditional cell phone or Wi-Fi router.
|
||||
The Meshtastic signal meter — the familiar bars or status color in the app — is calculated very differently than the "bars" on a traditional cell phone or WiFi router.
|
||||
|
||||
Most consumer devices simply measure how "loud" a signal is. However, because Meshtastic uses **LoRa (Long Range)** technology, its signal meter measures how **clear** the signal is, relative to the specific settings your mesh is using.
|
||||
|
||||
@@ -26,7 +26,7 @@ Every time the LoRa radio chip receives a message, it reports two measurements:
|
||||
* **RSSI (Received Signal Strength Indicator):** The **loudness** of the raw power hitting your antenna.
|
||||
* **SNR (Signal-to-Noise Ratio):** The **clarity** of the signal compared to the background static.
|
||||
|
||||
> **Tip — The Analogy:** Imagine you are trying to hear a friend talking to you.
|
||||
> 💡 **Tip:** Here's an analogy — imagine you are trying to hear a friend talking to you.
|
||||
> * **RSSI** is how loud their voice is.
|
||||
> * **The Noise Floor** is the background noise in the room (air conditioning, other people talking, traffic).
|
||||
> * **SNR** is how easily you can distinguish your friend's voice from the background noise.
|
||||
@@ -37,7 +37,7 @@ If your friend shouts at you at a deafening rock concert, the signal is incredib
|
||||
|
||||
## 2. The Magic of LoRa: Hearing "Below the Noise Floor"
|
||||
|
||||
For standard radios (like FM or Wi-Fi), if the background noise is louder than the signal (a negative SNR), the receiver just hears static.
|
||||
For standard radios (like FM or WiFi), if the background noise is louder than the signal (a negative SNR), the receiver just hears static.
|
||||
|
||||
LoRa is special. It uses **"Spread Spectrum"** modulation, which allows the radio to mathematically pull a signal out of the air even when it is buried deep *underneath* the background noise. This is why you will frequently see **negative SNR numbers** in Meshtastic (e.g., -10 dB, which means the signal is 10 decibels weaker than the background static).
|
||||
|
||||
@@ -47,16 +47,18 @@ Depending on which Meshtastic preset you are using (e.g., `LongFast` vs. `ShortF
|
||||
|
||||
## 3. How the Signal Meter Calculates Quality
|
||||
|
||||
The Meshtastic apps take both RSSI and SNR and run them through a specific formula to assign your signal a quality rating (None, Bad, Fair, or Good). It specifically scales these values based on the physical limits of the radio preset you are using.
|
||||
The app rates your signal quality (None, Bad, Fair, or Good) from **SNR alone, measured relative to the preset's SNR Limit** — the demodulation floor described above. It deliberately does **not** factor RSSI into the rating: without the local noise floor, RSSI cannot tell you whether a signal is actually decodable, so SNR-versus-the-preset-limit is the meaningful measure. (RSSI is still displayed to you elsewhere.)
|
||||
|
||||
Here is exactly how the app decides how many bars (or what color) to show you:
|
||||
Because the rating is relative to the preset limit, the *same* SNR can rate differently on different presets — `-15 dB` is healthy on `LongSlow` but unusable on `ShortFast`. Letting `limit` be the active preset's SNR Limit, here is how the app picks the bars (or color):
|
||||
|
||||
| Level | Bars | Criteria | Meaning |
|
||||
|-------|------|----------|---------|
|
||||
| Good | 3 | RSSI better than `-115 dBm` **AND** SNR better than `-7 dB` | Signal is both loud and clear — healthy connection. |
|
||||
| Fair | 2 | RSSI better than `-126 dBm` with good SNR, **OR** SNR better than `-15 dB` with good RSSI | Signal getting quieter or noisier, but still decodable. |
|
||||
| Bad | 1 | Falls between Fair and None thresholds | At the edge of range or experiencing interference. |
|
||||
| None | 0 | RSSI worse than `-126 dBm` **AND** SNR worse than `-15 dB` | Transmission completely buried in noise. |
|
||||
| Good | 3 | SNR **above** the preset's `limit` | Signal is comfortably above the demodulation floor — healthy connection. |
|
||||
| Fair | 2 | within `5.5 dB` below the `limit` | Decodable, but getting close to the floor. |
|
||||
| Bad | 1 | within `7.5 dB` below the `limit` | At the very edge of what the preset can recover. |
|
||||
| None | 0 | more than `7.5 dB` below the `limit` | Below the floor — transmission lost to noise. |
|
||||
|
||||
> **Note:** The fixed SNR thresholds you may have seen elsewhere (`-7 dB` / `-15 dB`) are now only used for coloring individual hops in traceroute results — not for the per-node signal meter described here.
|
||||
|
||||
---
|
||||
|
||||
@@ -64,9 +66,9 @@ Here is exactly how the app decides how many bars (or what color) to show you:
|
||||
|
||||
Because Meshtastic's meter acts as a **"Clarity Meter"**, it behaves differently than what most people expect:
|
||||
|
||||
> **Tip — Don't panic over low RSSI:** You might see a seemingly terrible RSSI value like `-118 dBm`. On a cell phone, you would have zero bars. But if you have an SNR of `+2 dB`, Meshtastic will still show a strong signal! *The library is quiet, so the whisper is heard perfectly.*
|
||||
> 💡 **Tip:** Don't panic over low RSSI. You might see a seemingly terrible RSSI value like `-118 dBm`. On a cell phone, you would have zero bars. But if you have an SNR of `+2 dB`, Meshtastic will still show a strong signal! *The library is quiet, so the whisper is heard perfectly.*
|
||||
|
||||
> **Warning — Watch out for local noise:** If you hook up a massive antenna and see a great RSSI (e.g., `-90 dBm`) but your signal meter is only showing **1 Bar (Bad)**, you have a problem. It means you have local interference — perhaps a cheap power supply, a noisy computer, or a nearby radio tower — creating so much static that it is drowning out your mesh.
|
||||
> ⚠️ **Warning:** Watch out for local noise. If you hook up a massive antenna and see a great RSSI (e.g., `-90 dBm`) but your signal meter is only showing **1 Bar (Bad)**, you have a problem. It means you have local interference — perhaps a cheap power supply, a noisy computer, or a nearby radio tower — creating so much static that it is drowning out your mesh.
|
||||
|
||||
## Where Signal Information Appears
|
||||
|
||||
@@ -79,3 +81,11 @@ In the app, signal data is shown in several places:
|
||||
|
||||

|
||||
|
||||
## Related Topics
|
||||
|
||||
- [Nodes](nodes) — where signal bars appear in the node list
|
||||
- [Node Metrics](node-metrics) — SNR/RSSI history and the per-node signal quality reference
|
||||
- [Settings — Radio & User](settings-radio-user) — modem presets and their SNR limits
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -48,6 +48,15 @@ The TAK module allows Meshtastic nodes to:
|
||||
2. Open ATAK and enable the Meshtastic plugin.
|
||||
3. The plugin bridges messages between ATAK and your mesh network.
|
||||
|
||||
### Local TAK Server
|
||||
|
||||
The app can also run a **local TAK server** so ATAK/iTAK clients on the same device or network can connect directly, without a remote TAK server. Open **Settings → Module Config → TAK → TAK Server**:
|
||||
|
||||

|
||||
|
||||
- **Enable Local TAK Server** — starts a local TLS server on port **8089** for ATAK/iTAK connections.
|
||||
- **Export TAK Data Package** — generates a `.zip` data package that ATAK/iTAK can import to connect to this server.
|
||||
|
||||
## TAK Roles
|
||||
|
||||
Nodes configured with TAK-related roles behave differently from standard clients:
|
||||
|
||||
@@ -103,11 +103,7 @@ Nodes with particulate matter or CO₂ sensors report air quality data:
|
||||
| PM10 | µg/m³ | Coarse particulate matter |
|
||||
| CO₂ | ppm | Carbon dioxide concentration |
|
||||
|
||||
The CO₂ reading is color-coded by severity:
|
||||
- 🟢 **Good** (< 1000 ppm) — normal indoor levels
|
||||
- 🟡 **Moderate** (1000–2000 ppm) — elevated, consider ventilation
|
||||
- 🟠 **Poor** (2000–5000 ppm) — drowsiness, poor concentration
|
||||
- 🔴 **Hazardous** (≥ 5000 ppm) — immediate health concern
|
||||
The CO₂ reading is color-coded by severity (Good → Stuffy → Poor → Unsafe → Evacuate). See [Node Metrics — Air Quality](node-metrics#air-quality-metrics) for the exact ppm bands and colors.
|
||||
|
||||
Air quality data can be viewed as info cards on the node detail screen, charted over time, and exported to CSV.
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
title: Translate the App
|
||||
parent: User Guide
|
||||
nav_order: 17
|
||||
last_updated: 2026-05-13
|
||||
last_updated: 2026-06-25
|
||||
description: How the app and its documentation are translated via Crowdin, and guidelines for contributing translations.
|
||||
aliases:
|
||||
- translate
|
||||
- crowdin
|
||||
@@ -23,7 +24,7 @@ Contributing translations helps make Meshtastic accessible to a wider audience.
|
||||
| User Guide pages | `docs/user/*.md` | In-app documentation shown in Help & Documentation |
|
||||
| Fastlane metadata | `fastlane/metadata/android/en-US/` | App Store listing title, description, and changelogs |
|
||||
|
||||
> **Note — Developer Guide pages are English-only.** Code-focused documentation targeting contributors is not translated.
|
||||
> ⚠️ **Note:** Developer Guide pages are English-only. Code-focused documentation targeting contributors is not translated.
|
||||
|
||||
---
|
||||
|
||||
@@ -35,7 +36,7 @@ Contributing translations helps make Meshtastic accessible to a wider audience.
|
||||
4. **Review context.** Many strings include screenshots or context comments — check these to understand where the text appears in the app.
|
||||
5. **Submit.** Approved translations are automatically merged into the next release.
|
||||
|
||||
> **Tip — Keep translations short.** UI strings often appear in buttons, chips, or narrow columns. If a translation is significantly longer than the English original, consider abbreviating where the meaning stays clear.
|
||||
> 💡 **Tip:** Keep translations short. UI strings often appear in buttons, chips, or narrow columns. If a translation is significantly longer than the English original, consider abbreviating where the meaning stays clear.
|
||||
|
||||
---
|
||||
|
||||
@@ -68,15 +69,19 @@ In-app documentation follows a similar pattern under `docs/`:
|
||||
|
||||
```
|
||||
docs/
|
||||
├── user/ ← English source (default)
|
||||
├── en/user/ ← English source (default)
|
||||
│ ├── onboarding.md
|
||||
│ └── ...
|
||||
├── fr/user/ ← French translations
|
||||
├── fr-rFR/user/ ← French (France)
|
||||
│ ├── onboarding.md
|
||||
│ └── ...
|
||||
├── de-rDE/user/ ← German (Germany)
|
||||
│ └── ...
|
||||
└── ...
|
||||
```
|
||||
|
||||
Locale folders use the Android resource convention `{lang}-r{REGION}` (e.g. `fr-rFR`, `de-rDE`, `ja-rJP`), matching the `values-*` directories used for app strings.
|
||||
|
||||
The app automatically selects the correct locale based on your device's **Language & Region** settings.
|
||||
|
||||
---
|
||||
|
||||
@@ -3,6 +3,7 @@ title: Units, Measurement & Locale
|
||||
parent: User Guide
|
||||
nav_order: 16
|
||||
last_updated: 2026-05-12
|
||||
description: How the app formats temperature, distance, speed, and other measurements based on your device locale.
|
||||
---
|
||||
|
||||
# Units, Measurement & Locale
|
||||
@@ -17,7 +18,7 @@ Meshtastic radios always transmit data in **metric units** (meters, °C, km/h, h
|
||||
|
||||
On Android, your measurement preferences are determined by your system **Language & Region** settings. On Desktop (JVM), the app uses the JVM's default `Locale`.
|
||||
|
||||
> **Tip — You never need to toggle units inside the app.** Change your system measurement preferences and every screen in Meshtastic updates automatically — node details, telemetry charts, weather, altitude, and more.
|
||||
> 💡 **Tip:** You never need to toggle units inside the app. Change your system measurement preferences and every screen in Meshtastic updates automatically — node details, telemetry charts, weather, altitude, and more.
|
||||
|
||||
---
|
||||
|
||||
@@ -114,5 +115,13 @@ On Android, your measurement system (metric vs imperial) is tied to your region
|
||||
2. Change your **Region** or **Measurement units** preference
|
||||
3. Return to Meshtastic — values update immediately
|
||||
|
||||
> **Tip — The app uses `MetricFormatter` from `core:common`.** All measurement formatting is handled by a shared KMP utility that respects your platform's locale. Developers adding new measurement displays should use `MetricFormatter` rather than hard-coding unit conversions.
|
||||
> 💡 **Tip:** The app uses `MetricFormatter` from `core:common`. All measurement formatting is handled by a shared KMP utility that respects your platform's locale. Developers adding new measurement displays should use `MetricFormatter` rather than hard-coding unit conversions.
|
||||
|
||||
## Related Topics
|
||||
|
||||
- [Node Metrics](node-metrics) — where temperature, distance, and sensor values are displayed
|
||||
- [Telemetry & Sensors](telemetry-and-sensors) — the sensors that produce these measurements
|
||||
- [Settings — Radio & User](settings-radio-user) — region setting that drives unit selection
|
||||
|
||||
---
|
||||
|
||||
|
||||
46
docs/en/user/widget.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: Home Screen Widget
|
||||
parent: User Guide
|
||||
nav_order: 20
|
||||
last_updated: 2026-06-25
|
||||
description: Add the Meshtastic home screen widget to glance at your connected radio's local stats without opening the app.
|
||||
aliases:
|
||||
- widget
|
||||
- home-screen-widget
|
||||
- local-stats-widget
|
||||
---
|
||||
|
||||
# Home Screen Widget
|
||||
|
||||
On Android, Meshtastic provides a home screen **widget** that shows live local statistics from your connected radio at a glance — no need to open the app.
|
||||
|
||||
## What It Shows
|
||||
|
||||
The widget displays the **connected radio's** current local stats:
|
||||
|
||||
- **Battery** — the radio's battery level, or *Powered* when running on external power
|
||||
- **ChUtil** — channel utilization (how busy the LoRa channel is, as a percentage)
|
||||
- **AirUtil** — airtime utilization (how much of the duty cycle your radio is transmitting)
|
||||
- **Traffic** — packets transmitted / received, and duplicates seen
|
||||
- **Relays** — packets relayed and relay cancellations (shown when the radio is relaying)
|
||||
|
||||
Tap the widget to open the app, or use its refresh control to request fresh stats.
|
||||
|
||||
> 💡 **Tip:** The values reflect the radio you are currently connected to. If the app isn't connected to a radio, the widget shows the last known stats until it reconnects.
|
||||
|
||||
## Adding the Widget
|
||||
|
||||
1. Long-press an empty area of your Android home screen.
|
||||
2. Tap **Widgets**.
|
||||
3. Find **Meshtastic** in the list and drag the **Local Stats** widget to your home screen.
|
||||
4. Resize it as needed — the layout adapts to the available space.
|
||||
|
||||
> ⚠️ **Note:** The widget is Android-only. It is not available on the Desktop or iOS builds.
|
||||
|
||||
## Related Topics
|
||||
|
||||
- [Node Metrics](node-metrics) — the full Signal Quality and Local Stats history inside the app
|
||||
- [Connections](connections) — connect to a radio so the widget has stats to show
|
||||
- [Discovery](discovery) — channel and airtime utilization across the mesh
|
||||
|
||||
---
|
||||
@@ -3,6 +3,7 @@
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>Kdoc:TcpDiscoveryHelpers.kt:/** * Shared helpers for TCP device discovery logic used by both [CommonGetDiscoveredDevicesUseCase] and the * Android-specific variant. */</ID>
|
||||
<ID>PreviewPublic:ConnectionsPreviews.kt:@PreviewLightDark @Composable fun BluetoothScanPreview</ID>
|
||||
<ID>PreviewPublic:ConnectionsPreviews.kt:@PreviewLightDark @Composable fun ConnectingDeviceInfoPreview</ID>
|
||||
<ID>PreviewPublic:ConnectionsPreviews.kt:@PreviewLightDark @Composable fun DeviceListItemPreview</ID>
|
||||
<ID>PreviewPublic:ConnectionsPreviews.kt:@PreviewLightDark @Composable fun DeviceSectionHeaderPreview</ID>
|
||||
|
||||
@@ -16,8 +16,13 @@
|
||||
*/
|
||||
package org.meshtastic.feature.connections.component
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
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 kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -71,7 +76,43 @@ fun ConnectingDeviceInfoPreview() {
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
fun EmptyStateContentPreview() {
|
||||
AppTheme { EmptyStateContent(text = "No devices found", imageVector = MeshtasticIcons.Search) }
|
||||
// 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
|
||||
@Composable
|
||||
fun BluetoothScanPreview() {
|
||||
AppTheme {
|
||||
Surface(modifier = Modifier.fillMaxWidth().height(320.dp)) {
|
||||
DeviceList(
|
||||
connectionState = ConnectionState.Disconnected,
|
||||
selectedDevice = "",
|
||||
bleDevices =
|
||||
listOf(
|
||||
DeviceListEntry.Ble(PreviewBleDevice(address = "AA:BB:CC:DD:EE:FF", name = "Meshtastic_abcd")),
|
||||
),
|
||||
usbDevices = emptyList(),
|
||||
discoveredTcpDevices = emptyList(),
|
||||
recentTcpDevices = emptyList(),
|
||||
isBleScanning = true,
|
||||
isNetworkScanning = false,
|
||||
activeTransport = DeviceType.BLE,
|
||||
onSelectDevice = {},
|
||||
onToggleBleScan = {},
|
||||
onToggleNetworkScan = {},
|
||||
onAddManualAddress = { _, _ -> },
|
||||
onRemoveRecentAddress = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
|
||||
@@ -16,9 +16,7 @@
|
||||
*/
|
||||
package org.meshtastic.feature.firmware
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -29,16 +27,15 @@ import androidx.compose.ui.unit.dp
|
||||
import org.meshtastic.core.resources.UiText
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
|
||||
// These previews intentionally wrap-content (no fillMaxSize) so the generated reference images are tight crops of
|
||||
// the status block — the docs reference the status component itself, not the whole screen. See docs/assets/screenshots.
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
fun VerifyingStatePreview() {
|
||||
AppTheme {
|
||||
Surface {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Column(modifier = Modifier.padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
VerifyingState()
|
||||
}
|
||||
}
|
||||
@@ -50,11 +47,7 @@ fun VerifyingStatePreview() {
|
||||
fun CheckingStatePreview() {
|
||||
AppTheme {
|
||||
Surface {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Column(modifier = Modifier.padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
CheckingState()
|
||||
}
|
||||
}
|
||||
@@ -66,11 +59,7 @@ fun CheckingStatePreview() {
|
||||
fun ErrorStatePreview() {
|
||||
AppTheme {
|
||||
Surface {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Column(modifier = Modifier.padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
ErrorState(error = UiText.DynamicString("Connection lost"), onRetry = {})
|
||||
}
|
||||
}
|
||||
@@ -82,11 +71,7 @@ fun ErrorStatePreview() {
|
||||
fun SuccessStatePreview() {
|
||||
AppTheme {
|
||||
Surface {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Column(modifier = Modifier.padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
SuccessState(onDone = {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,6 @@ dependencies {
|
||||
implementation(project(":feature:wifi-provision"))
|
||||
implementation(project(":feature:connections"))
|
||||
implementation(project(":feature:settings"))
|
||||
implementation(project(":feature:firmware"))
|
||||
implementation(project(":feature:intro"))
|
||||
implementation(project(":feature:map"))
|
||||
implementation(project(":feature:docs"))
|
||||
@@ -68,6 +67,8 @@ tasks.register<Copy>("copyDocsScreenshots") {
|
||||
group = "documentation"
|
||||
|
||||
val referenceDir = layout.projectDirectory.dir("src/screenshotTestDebug/reference")
|
||||
// Doc-framed compositions live in the generate-only :docs-screenshots module; aggregate its references too.
|
||||
val docsReferenceDir = rootProject.layout.projectDirectory.dir("docs-screenshots/src/screenshotTestDebug/reference")
|
||||
val manifestFile = layout.projectDirectory.file("docs-screenshots-manifest.txt")
|
||||
val aliasFile = layout.projectDirectory.file("docs-screenshot-aliases.properties")
|
||||
val outputDir = rootProject.layout.projectDirectory.dir("docs/assets/screenshots")
|
||||
@@ -83,6 +84,7 @@ tasks.register<Copy>("copyDocsScreenshots") {
|
||||
}
|
||||
|
||||
from(referenceDir) { include(manifestPatterns) }
|
||||
from(docsReferenceDir) { include(manifestPatterns) }
|
||||
into(outputDir)
|
||||
|
||||
// Build reverse alias map (CST name → semantic name) for renaming during copy.
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
onboarding_welcome.png=ScreenshotWelcomeScreen_Light_b29dc7a7_0.png
|
||||
|
||||
# Connections
|
||||
connections_bluetooth_scan.png=ScreenshotScanningBle_Light_b29dc7a7_0.png
|
||||
connections_bluetooth_scan.png=ScreenshotConnectionsBluetoothScan_Light_b29dc7a7_0.png
|
||||
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
|
||||
@@ -22,10 +22,12 @@ firmware_checking.png=ScreenshotFirmwareChecking_Light_b29dc7a7_0.png
|
||||
firmware_disclaimer.png=ScreenshotFirmwareDisclaimer_Light_b29dc7a7_0.png
|
||||
firmware_error.png=ScreenshotFirmwareError_Light_b29dc7a7_0.png
|
||||
firmware_success.png=ScreenshotFirmwareSuccess_Light_b29dc7a7_0.png
|
||||
firmware_verifying.png=ScreenshotFirmwareVerifying_Light_b29dc7a7_0.png
|
||||
|
||||
# Messages
|
||||
messages_quick_chat.png=ScreenshotQuickChatItem_Light_b29dc7a7_0.png
|
||||
messages_reaction.png=ScreenshotReactionItem_Light_b29dc7a7_0.png
|
||||
messages_edit_quick_chat.png=ScreenshotEditQuickChatDialog_Light_b29dc7a7_0.png
|
||||
messages_search_bar.png=ScreenshotMessageSearchBar_Light_b29dc7a7_0.png
|
||||
messages-and-channels_channel_list.png=ScreenshotChannelItem_Light_b29dc7a7_0.png
|
||||
|
||||
@@ -46,6 +48,7 @@ nodes_distance_info.png=ScreenshotDistanceInfo_Light_b29dc7a7_0.png
|
||||
# Node metrics
|
||||
node-metrics_telemetric_actions.png=ScreenshotTelemetricActionsSection_Light_b29dc7a7_0.png
|
||||
node-metrics_air_quality.png=ScreenshotAirQualityCards_Light_b29dc7a7_0.png
|
||||
node-metrics_iaq_scale.png=ScreenshotIAQScale_Light_b29dc7a7_0.png
|
||||
|
||||
# Discovery (Local Mesh Discovery scanner)
|
||||
discovery_preset_result.png=ScreenshotDiscoveryPresetResult_Light_b29dc7a7_0.png
|
||||
@@ -73,13 +76,11 @@ settings_password_field.png=ScreenshotEditPasswordPreference_Light_b29dc7a7_0.pn
|
||||
settings_text_field.png=ScreenshotEditTextPreference_Light_b29dc7a7_0.png
|
||||
settings_ipv4_field.png=ScreenshotEditIPv4Preference_Light_b29dc7a7_0.png
|
||||
|
||||
# TAK
|
||||
tak_server_enabled.png=ScreenshotTakServerSectionEnabled_Light_b29dc7a7_0.png
|
||||
|
||||
# Docs browser
|
||||
docs-browser_toc.png=ScreenshotDocsBrowser_Light_b29dc7a7_0.png
|
||||
docs-browser_search.png=ScreenshotDocsSearchBarWithQuery_Light_b29dc7a7_0.png
|
||||
docs-browser_page.png=ScreenshotDocsPageContent_Light_b29dc7a7_0.png
|
||||
docs-browser_chirpy.png=ScreenshotChirpyAssistant_Light_b29dc7a7_0.png
|
||||
|
||||
# NOTE: connections_wifi_scanning.png, connections_wifi_device_found.png, and
|
||||
# connections_wifi_success.png are manual captures with no current CST source;
|
||||
# they remain hand-maintained in docs/assets/screenshots/ until WifiProvision
|
||||
# previews matching the documented flow exist.
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
|
||||
# Feature: Connections
|
||||
**/ConnectionsScreenshotTestsKt/Screenshot*_Light_*.png
|
||||
# Feature: Connections — doc-framed compositions (generated by :docs-screenshots)
|
||||
**/ConnectionsDocScreenshotTestsKt/Screenshot*_Light_*.png
|
||||
|
||||
# Feature: Firmware
|
||||
**/FirmwareScreenshotTestsKt/Screenshot*_Light_*.png
|
||||
|
||||
@@ -23,7 +23,6 @@ import org.meshtastic.feature.connections.component.ConnectingDeviceInfoPreview
|
||||
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.EmptyStateContentPreview
|
||||
import org.meshtastic.feature.connections.component.TransportSelectorPreview
|
||||
|
||||
@PreviewTest
|
||||
@@ -47,13 +46,6 @@ fun ScreenshotConnectingDeviceInfo() {
|
||||
ConnectingDeviceInfoPreview()
|
||||
}
|
||||
|
||||
@PreviewTest
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
fun ScreenshotEmptyStateContent() {
|
||||
EmptyStateContentPreview()
|
||||
}
|
||||
|
||||
@PreviewTest
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
|
||||
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 38 KiB |
@@ -134,5 +134,6 @@ include(
|
||||
":core:barcode",
|
||||
":feature:widget",
|
||||
":screenshot-tests",
|
||||
":docs-screenshots",
|
||||
":baselineprofile",
|
||||
)
|
||||
|
||||