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>
This commit is contained in:
James Rich
2026-06-25 20:46:32 -05:00
parent 307d4eba3d
commit 8346b18cc4
71 changed files with 444 additions and 134 deletions

View File

@@ -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');

View File

@@ -37,6 +37,7 @@ jobs:
- 'core/**'
- 'feature/**'
- 'screenshot-tests/**'
- 'docs-screenshots/**'
# Shared build infrastructure
- 'build-logic/**'
- 'config/**'

View File

@@ -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.

View File

@@ -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

View 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)
}

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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.

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -38,6 +38,8 @@ Keep the last 58 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.

View File

@@ -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/

View File

@@ -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).

View File

@@ -19,6 +19,10 @@ Documentation for using the Meshtastic Android and Desktop app.
Keep the last 58 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.

View File

@@ -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.
![Device list item](../../assets/screenshots/connections_bluetooth_scan.png)
![Scanning for Bluetooth devices, with a discovered radio in the list](../../assets/screenshots/connections_bluetooth_scan.png)
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).
![WiFi scanning for devices](../../assets/screenshots/connections_wifi_scanning.png)
When a device is found, it appears in the connection list:
![WiFi device found](../../assets/screenshots/connections_wifi_device_found.png)
A successful connection is confirmed with a status indicator:
![WiFi connection success](../../assets/screenshots/connections_wifi_success.png)
> 💡 **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

View File

@@ -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:
![Verifying update and waiting for the device to reconnect](../../assets/screenshots/firmware_verifying.png)
Once the update succeeds:
- The radio will reboot automatically
- Bluetooth connection will re-establish
- Verify your settings are intact

View 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.
![In-app documentation browser table of contents](../../assets/screenshots/docs-browser_toc.png)
### Search
Tap the search icon and type to filter pages by title and keywords — results update as you type.
![Searching the in-app documentation](../../assets/screenshots/docs-browser_search.png)
A page open in the browser:
![A documentation page rendered in the app](../../assets/screenshots/docs-browser_page.png)
## 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.
![Chirpy AI assistant answering a question with page links](../../assets/screenshots/docs-browser_chirpy.png)
> 🔒 **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)
---

View File

@@ -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

View File

@@ -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:
![Quick chat option](../../assets/screenshots/messages_quick_chat.png)
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:
![New quick chat dialog with name, message, and instantly-send toggle](../../assets/screenshots/messages_edit_quick_chat.png)
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

View File

@@ -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 0500+ value derived from gas resistance, shown against a color-coded scale from *Excellent* to *Dangerously Polluted*:
![IAQ index scale from Excellent to Dangerously Polluted](../../assets/screenshots/node-metrics_iaq_scale.png)
> 💡 **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.

View File

@@ -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) |

View File

@@ -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 |

View File

@@ -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:
![Node entry showing SNR, RSSI values and colored signal bars](../../assets/screenshots/nodes_signal_info.png)
## 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
---

View File

@@ -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**:
![Local TAK Server settings with enable toggle and export option](../../assets/screenshots/tak_server_enabled.png)
- **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:

View File

@@ -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** (10002000 ppm) — elevated, consider ventilation
- 🟠 **Poor** (20005000 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.

View File

@@ -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.
---

View File

@@ -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
View 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
---

View File

@@ -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>

View File

@@ -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

View File

@@ -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 = {})
}
}

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -134,5 +134,6 @@ include(
":core:barcode",
":feature:widget",
":screenshot-tests",
":docs-screenshots",
":baselineprofile",
)