diff --git a/.skills/compose-ui/strings-index.txt b/.skills/compose-ui/strings-index.txt index d48dc0c22c..a3484428d1 100644 --- a/.skills/compose-ui/strings-index.txt +++ b/.skills/compose-ui/strings-index.txt @@ -429,6 +429,8 @@ distance_measurements_description dns ### DOC ### doc_clear_search +doc_keywords_android_auto +doc_keywords_app_functions doc_keywords_connections doc_keywords_desktop doc_keywords_discovery @@ -450,6 +452,8 @@ doc_keywords_units doc_search_placeholder doc_section_developer doc_section_user +doc_title_android_auto +doc_title_app_functions doc_title_connections doc_title_desktop doc_title_discovery diff --git a/.skills/new-branch/SKILL.md b/.skills/new-branch/SKILL.md index d63f3f4c27..60d81d234a 100644 --- a/.skills/new-branch/SKILL.md +++ b/.skills/new-branch/SKILL.md @@ -16,9 +16,7 @@ This replaces the ad-hoc prose that used to be retyped at the start of every ses 1. **Clean worktree.** If `git status --porcelain` is non-empty, ask the user before proceeding. 2. **Upstream remote present.** `git remote -v` must list `upstream` pointing at `meshtastic/Meshtastic-Android`. If only `origin` exists on a fork, treat `origin` as upstream. -3. **Submodules initialised.** `core/proto/src/main/proto` must be populated — see AGENTS.md - workspace bootstrap rules. -4. **Secrets bootstrapped.** If `local.properties` is missing, copy `secrets.defaults.properties` +3. **Secrets bootstrapped.** If `local.properties` is missing, copy `secrets.defaults.properties` (required for `google` flavor builds). ## Standard Recipe @@ -30,10 +28,7 @@ git fetch upstream --prune --tags # 2. Create the branch from upstream/main (never from a local stale main) git switch -c upstream/main -# 3. Ensure submodules track the new base -git submodule update --init --recursive - -# 4. Sanity check +# 3. Sanity check git --no-pager log -1 --oneline ``` @@ -58,7 +53,6 @@ When the user says *"rebase #NNNN"* or *"dust off PR NNNN"*: git fetch upstream --prune gh pr checkout # checks out the PR head locally git rebase upstream/main -git submodule update --init --recursive # Resolve conflicts, then: git push --force-with-lease ``` @@ -67,7 +61,6 @@ Never use plain `--force`. Always `--force-with-lease` to avoid clobbering colla ## Post-Branch Checklist - [ ] Branch name follows conventional prefix. -- [ ] Submodules up to date. - [ ] `local.properties` exists. - [ ] `ANDROID_HOME` exported (see AGENTS.md workspace bootstrap). - [ ] Optional: run `./gradlew assembleDebug` once to catch environment regressions before editing. diff --git a/.skills/project-overview/SKILL.md b/.skills/project-overview/SKILL.md index 3869243072..89146c197f 100644 --- a/.skills/project-overview/SKILL.md +++ b/.skills/project-overview/SKILL.md @@ -5,7 +5,7 @@ Module directory, namespacing conventions, environment setup, and troubleshootin - **Build System:** Gradle (Kotlin DSL). JDK 21 REQUIRED. Target SDK: API 36. Min SDK: API 26. - **Flavors:** `fdroid` (OSS only) · `google` (Maps + DataDog analytics) -- **Android-only Modules:** `core:barcode` (CameraX). Shared contracts abstracted into `core:ui/commonMain`. +- **Android-only Modules:** `core:barcode` (CameraX), `feature:widget` (Glance home-screen widget), `feature:car` (Android Auto via the Car App Library, `google` flavor only), and `baselineprofile` (Macrobenchmark). Shared contracts are abstracted into `core:ui/commonMain`. ## Codebase Map @@ -16,7 +16,6 @@ Module directory, namespacing conventions, environment setup, and troubleshootin | `config/` | Detekt static analysis rules (`config/detekt/detekt.yml`) and Spotless formatting config (`config/spotless/.editorconfig`). | | `docs/` | Architecture docs and agent playbooks. See `docs/kmp-status.md` and `docs/roadmap.md` for current status. | | `core/model` | Domain models and common data structures. | -| `core:proto` | Protobuf definitions (Git submodule). | | `core:common` | Low-level utilities, I/O abstractions (Okio), and common types. | | `core:database` | Room KMP database implementation. | | `core:datastore` | Multiplatform DataStore for preferences. | @@ -28,13 +27,15 @@ Module directory, namespacing conventions, environment setup, and troubleshootin | `core:navigation` | Shared navigation keys/routes for Navigation 3 using `@Serializable sealed interface` hierarchies. `DeepLinkRouter` for typed backstack synthesis, and `MeshtasticNavSavedStateConfig` with `subclassesOfSealed()` for automatic polymorphic backstack persistence. | | `core:ui` | Shared Compose UI components (`MeshtasticAppShell`, `MeshtasticNavDisplay`, `MeshtasticNavigationSuite`, `AlertHost`, `SharedDialogs`, `PlaceholderScreen`, `MainAppBar`, dialogs, preferences) and platform abstractions. | | `core:service` | KMP service layer; Android bindings stay in `androidMain`. | +| `core:takserver` | Meshtastic ↔ TAK (ATAK/iTAK) bridge — local CoT server and CoT ⇄ mesh conversion. | | `core:prefs` | KMP preferences layer built on DataStore abstractions. | | `core:barcode` | Barcode scanning (Android-only). | | `core:nfc` | NFC abstractions (KMP). Android NFC hardware implementation in `androidMain`. | | `core/ble/` | Bluetooth Low Energy stack using Kable. | | `core/resources/` | Centralized string and image resources (Compose Multiplatform). | | `core/testing/` | Shared test doubles, fakes, and utilities for `commonTest` across all KMP modules. | -| `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`, `node`, `intro`, `connections`, `firmware`, `wifi-provision`, `widget`). All are KMP except `widget`. Use `meshtastic.kmp.feature` convention plugin. | +| `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`, `node`, `intro`, `connections`, `firmware`, `wifi-provision`, `discovery`, `docs`, `widget`, `car`). Most are KMP and use the `meshtastic.kmp.feature` convention plugin; `widget` (Glance) and `car` (Android Auto, `google` flavor only) are Android-only. | +| `baselineprofile/` | Macrobenchmark Baseline Profile generation for `:androidApp` (AOT-compiled cold-start journey). Android-only. | | `feature/wifi-provision` | KMP WiFi provisioning via BLE (Nymea protocol). Uses `core:ble` Kable abstractions. | | `feature/firmware` | Fully KMP firmware update system: Unified OTA (BLE + WiFi), native Nordic Secure DFU protocol (pure KMP), USB/UF2 updates, and `FirmwareRetriever` with manifest-based resolution. Desktop is a first-class target. | | `desktopApp/` | Compose Desktop application. Thin host shell relying on feature modules for shared UI. Full Koin DI graph, TCP, Serial/USB, and BLE transports. Versioning via `config.properties` + `GitVersionValueSource`. | @@ -66,12 +67,7 @@ Agents **MUST** perform these steps automatically at the start of every session ``` All `./gradlew` invocations must include `ANDROID_HOME` in the environment. If the SDK cannot be found, ask the user for the path. -2. **Proto submodule:** `core/proto/src/main/proto` is a Git submodule containing Protobuf definitions. It must be initialized or builds will fail with proto generation errors: - ```bash - git submodule update --init - ``` - -3. **Init secrets:** If `local.properties` does not exist, copy `secrets.defaults.properties` to `local.properties`. Without this the `google` flavor build fails: +2. **Init secrets:** If `local.properties` does not exist, copy `secrets.defaults.properties` to `local.properties`. Without this the `google` flavor build fails: ```bash [ -f local.properties ] || cp secrets.defaults.properties local.properties ``` diff --git a/AGENTS.md b/AGENTS.md index fb0661d4ec..4ccb053ea5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,7 +38,7 @@ You are an expert Android/KMP engineer. Maintain architectural boundaries, use M - **CMP Over Android:** Use `compose-multiplatform` constraints. Pre-format floats with `NumberFormatter.format()`. Use `MeshtasticNavDisplay` and `NavigationBackHandler`. - **Zero Lint Tolerance:** Task is incomplete if `detekt` or `spotlessCheck` fails. - **Verify Before Push:** Treat any "push" as verify-then-push. CI has failed repeatedly due to skipped local checks. -- **Never Touch Protos or Secrets:** `core/proto` is an upstream submodule. Secrets are git-ignored. +- **Never Touch Protos or Secrets:** Protobuf models come from the upstream `org.meshtastic:protobufs` Maven dependency (pinned in `gradle/libs.versions.toml`) — bump the version upstream, never hand-edit generated proto. Secrets are git-ignored. - **Privacy First:** Never log or expose PII, location, or cryptographic keys. diff --git a/CLAUDE.md b/CLAUDE.md index 0e54c6487b..a97903bb89 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,7 +22,6 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co JDK 21 is required. **Bootstrap before any Gradle task** (don't wait to be told) — full details in `.skills/project-overview/SKILL.md`: ```bash [ -z "$ANDROID_HOME" ] && export ANDROID_HOME="$HOME/Library/Android/sdk" # often unset in agent workspaces -git submodule update --init # proto submodule; builds fail without it [ -f local.properties ] || cp secrets.defaults.properties local.properties # google flavor fails without it ``` diff --git a/README.md b/README.md index c08aa3ac07..342400ed03 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,16 @@ This is a tool for using Android (and Compose Desktop) with open-source mesh rad If you have questions or feedback please [Join our discussion forum](https://github.com/orgs/meshtastic/discussions) or the [Discord Group](https://discord.gg/meshtastic). We would love to hear from you! +## Features +Highlights from the latest release: + +- **Full-text message search** across your conversation history. +- **Mesh network discovery** to surface nodes and channels around you. +- **Android Auto** support for hands-free use while driving (`google` flavor). +- **Air-quality telemetry** — PM1.0, PM2.5, PM10, and CO₂ readings from supported sensors. +- **Device hardware links** via [msh.to](https://msh.to) for quick access to hardware details. +- **App Functions / system-AI integration** so on-device assistants can trigger common workflows. ## Get Meshtastic @@ -76,7 +85,7 @@ The app follows modern Android development practices, built on top of a shared K - **State Management:** Unidirectional Data Flow (UDF) with ViewModels, Coroutines, and Flow. - **Dependency Injection:** Koin with Koin Annotations (K2 Compiler Plugin). - **Navigation:** JetBrains Navigation 3 (Multiplatform routing with RESTful deep linking). -- **Data Layer:** Repository pattern with Room KMP (local DB), DataStore (prefs), and Protobuf (device comms). +- **Data Layer:** Repository pattern with Room KMP (local DB), DataStore (prefs), and Protobuf (device comms). Protobuf models are consumed from the upstream `org.meshtastic:protobufs` Maven artifact, pinned in `gradle/libs.versions.toml`. ### Bluetooth Low Energy (BLE) The BLE stack uses a multiplatform interface-driven architecture. Platform-agnostic interfaces live in `commonMain`, utilizing the **Kable** multiplatform BLE library to handle device communication across all supported targets (Android, Desktop). This provides a robust, Coroutine-based architecture for reliable device communication while remaining fully KMP compatible. See [core/ble/README.md](core/ble/README.md) for details. @@ -106,7 +115,6 @@ Each module has its own README with details on its responsibilities, API surface | [core/nfc](core/nfc/README.md) | NFC support | | [core/prefs](core/prefs/README.md) | Legacy preference helpers | | [core/barcode](core/barcode/README.md) | Barcode / QR scanning | -| [core/proto](core/proto/README.md) | Protobuf submodule wrapper | | [feature/messaging](feature/messaging/README.md) | Messaging UI feature | | [feature/map](feature/map/README.md) | Map UI feature | | [feature/node](feature/node/README.md) | Node detail UI feature | @@ -115,8 +123,11 @@ Each module has its own README with details on its responsibilities, API surface | [feature/intro](feature/intro/README.md) | Onboarding / intro UI feature | | [feature/wifi-provision](feature/wifi-provision/README.md) | Wi-Fi provisioning UI feature | | [feature/connections](feature/connections/README.md) | Device discovery & connection management (BLE / USB / TCP) | +| [feature/discovery](feature/discovery) | Mesh network discovery | | [feature/docs](feature/docs/README.md) | In-app documentation browser with Chirpy AI assistant | | [feature/widget](feature/widget/README.md) | Android home-screen Glance widget (live mesh stats) | +| [feature/car](feature/car) | Android Auto integration (Car App Library, `google` flavor) | +| [baselineprofile](baselineprofile/README.md) | Macrobenchmark Baseline Profile generation for `:androidApp` | ## Translations diff --git a/core/resources/src/commonMain/composeResources/values/strings.xml b/core/resources/src/commonMain/composeResources/values/strings.xml index 022ea3aeae..5407e39537 100644 --- a/core/resources/src/commonMain/composeResources/values/strings.xml +++ b/core/resources/src/commonMain/composeResources/values/strings.xml @@ -453,6 +453,8 @@ DNS Clear search + android auto,car,head unit,driving,hands free,messaging + system ai,gemini,assistant,functions,automation,voice bluetooth,usb,tcp,pairing,serial,wifi desktop,linux,macos,windows,serial discovery,topology,network,scan,neighbor @@ -474,6 +476,8 @@ Search documentation… Developer Guide User Guide + Android Auto + App Functions Connections Desktop App Discovery diff --git a/docs/assets/screenshots/README.md b/docs/assets/screenshots/README.md index edf5ca8d1c..ab3b37eb30 100644 --- a/docs/assets/screenshots/README.md +++ b/docs/assets/screenshots/README.md @@ -1,25 +1,39 @@ # Screenshots -This directory contains screenshot assets referenced by the documentation pages. +This directory is the **single source of truth** for screenshot assets referenced by the +documentation pages. It is consumed by both: -Screenshots are sourced from the Compose Preview Screenshot Testing reference images -in `screenshot-tests/src/screenshotTestDebug/reference/`. Light-mode variants are -copied here for use by the Jekyll docs site and in-app documentation browser. +- the **Jekyll docs site** (markdown references `../../assets/screenshots/{name}.png`), and +- the **in-app docs browser** — `:feature:docs:syncDocsToComposeResources` bundles this + directory into compose resources at `files/docs/assets/screenshots/`. + +`DocImageWiringTest` (in `:feature:docs`) fails the build if a doc page references an image +that is not present here. ## Updating Screenshots -After changing a UI component, regenerate reference images and copy them here: +Most screenshots are generated from the Compose Preview Screenshot Testing reference images +in `screenshot-tests/src/screenshotTestDebug/reference/`. After changing a UI component: ```bash -./gradlew :screenshot-tests:updateDebugScreenshotTest +./gradlew :screenshot-tests:updateDebugScreenshotTest # regenerate reference images +./gradlew :screenshot-tests:copyDocsScreenshots # refresh this directory ``` -Then copy the relevant light-mode PNGs from the reference directory. The -`copyDocsScreenshots` task automates bulk copying based on the manifest: +`copyDocsScreenshots` copies **only** the light-mode reference images that have a semantic +alias in `screenshot-tests/docs-screenshot-aliases.properties`, renaming them on the way. +Commit the refreshed PNGs together with the reference-image changes. -```bash -./gradlew :screenshot-tests:copyDocsScreenshots -``` +## 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`. +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. ## Naming Convention @@ -27,14 +41,12 @@ Then copy the relevant light-mode PNGs from the reference directory. The {page-id}_{description}.png ``` -Examples: -- `onboarding_welcome.png` -- `connections_bluetooth_scan.png` -- `firmware_disclaimer.png` +Examples: `onboarding_welcome.png`, `connections_bluetooth_scan.png`, `discovery_preset_result.png`. ## Guidelines -- PNG format, light-mode only (dark variants live in reference directory) +- 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 diff --git a/docs/assets/screenshots/app-functions_settings.png b/docs/assets/screenshots/app-functions_settings.png new file mode 100644 index 0000000000..d3cfed4378 Binary files /dev/null and b/docs/assets/screenshots/app-functions_settings.png differ diff --git a/docs/assets/screenshots/connections_bluetooth_scan.png b/docs/assets/screenshots/connections_bluetooth_scan.png index 6377595580..67df31f38b 100644 Binary files a/docs/assets/screenshots/connections_bluetooth_scan.png and b/docs/assets/screenshots/connections_bluetooth_scan.png differ diff --git a/docs/assets/screenshots/connections_connecting.png b/docs/assets/screenshots/connections_connecting.png index 1fc10a657f..573d8b84d4 100644 Binary files a/docs/assets/screenshots/connections_connecting.png and b/docs/assets/screenshots/connections_connecting.png differ diff --git a/docs/assets/screenshots/connections_empty_state.png b/docs/assets/screenshots/connections_empty_state.png index 322ac11620..096d5690bd 100644 Binary files a/docs/assets/screenshots/connections_empty_state.png and b/docs/assets/screenshots/connections_empty_state.png differ diff --git a/docs/assets/screenshots/discovery_dwell_progress.png b/docs/assets/screenshots/discovery_dwell_progress.png new file mode 100644 index 0000000000..83dac44294 Binary files /dev/null and b/docs/assets/screenshots/discovery_dwell_progress.png differ diff --git a/docs/assets/screenshots/discovery_preset_result.png b/docs/assets/screenshots/discovery_preset_result.png new file mode 100644 index 0000000000..04e4612f47 Binary files /dev/null and b/docs/assets/screenshots/discovery_preset_result.png differ diff --git a/docs/assets/screenshots/docs-browser_chirpy.png b/docs/assets/screenshots/docs-browser_chirpy.png index 91c2f77b43..2f2f78fa44 100644 Binary files a/docs/assets/screenshots/docs-browser_chirpy.png and b/docs/assets/screenshots/docs-browser_chirpy.png differ diff --git a/docs/assets/screenshots/docs-browser_page.png b/docs/assets/screenshots/docs-browser_page.png index e9d99045d4..d19940d514 100644 Binary files a/docs/assets/screenshots/docs-browser_page.png and b/docs/assets/screenshots/docs-browser_page.png differ diff --git a/docs/assets/screenshots/docs-browser_toc.png b/docs/assets/screenshots/docs-browser_toc.png index 4bda5f9a4f..857fc0fcf3 100644 Binary files a/docs/assets/screenshots/docs-browser_toc.png and b/docs/assets/screenshots/docs-browser_toc.png differ diff --git a/docs/assets/screenshots/firmware_checking.png b/docs/assets/screenshots/firmware_checking.png index 6a60c726dd..a22d68b3b7 100644 Binary files a/docs/assets/screenshots/firmware_checking.png and b/docs/assets/screenshots/firmware_checking.png differ diff --git a/docs/assets/screenshots/firmware_disclaimer.png b/docs/assets/screenshots/firmware_disclaimer.png index 322df1f5d5..346e865b1f 100644 Binary files a/docs/assets/screenshots/firmware_disclaimer.png and b/docs/assets/screenshots/firmware_disclaimer.png differ diff --git a/docs/assets/screenshots/firmware_error.png b/docs/assets/screenshots/firmware_error.png index 5ddb4d376d..30c34d13e2 100644 Binary files a/docs/assets/screenshots/firmware_error.png and b/docs/assets/screenshots/firmware_error.png differ diff --git a/docs/assets/screenshots/messages-and-channels_channel_list.png b/docs/assets/screenshots/messages-and-channels_channel_list.png new file mode 100644 index 0000000000..f7d7130169 Binary files /dev/null and b/docs/assets/screenshots/messages-and-channels_channel_list.png differ diff --git a/docs/assets/screenshots/messages_quick_chat.png b/docs/assets/screenshots/messages_quick_chat.png index 9418710db3..3d2c8bf35f 100644 Binary files a/docs/assets/screenshots/messages_quick_chat.png and b/docs/assets/screenshots/messages_quick_chat.png differ diff --git a/docs/assets/screenshots/messages_search_bar.png b/docs/assets/screenshots/messages_search_bar.png new file mode 100644 index 0000000000..8c9b0ae67d Binary files /dev/null and b/docs/assets/screenshots/messages_search_bar.png differ diff --git a/docs/assets/screenshots/node-metrics_air_quality.png b/docs/assets/screenshots/node-metrics_air_quality.png new file mode 100644 index 0000000000..3e177a008b Binary files /dev/null and b/docs/assets/screenshots/node-metrics_air_quality.png differ diff --git a/docs/assets/screenshots/nodes_battery_info.png b/docs/assets/screenshots/nodes_battery_info.png index b936f8b183..b7be0b8478 100644 Binary files a/docs/assets/screenshots/nodes_battery_info.png and b/docs/assets/screenshots/nodes_battery_info.png differ diff --git a/docs/assets/screenshots/nodes_detail_minimal.png b/docs/assets/screenshots/nodes_detail_minimal.png new file mode 100644 index 0000000000..fc5a4913a4 Binary files /dev/null and b/docs/assets/screenshots/nodes_detail_minimal.png differ diff --git a/docs/assets/screenshots/nodes_device_metrics_card.png b/docs/assets/screenshots/nodes_device_metrics_card.png new file mode 100644 index 0000000000..b84ea8db47 Binary files /dev/null and b/docs/assets/screenshots/nodes_device_metrics_card.png differ diff --git a/docs/assets/screenshots/nodes_environment_metrics.png b/docs/assets/screenshots/nodes_environment_metrics.png index bd01926438..8df596a816 100644 Binary files a/docs/assets/screenshots/nodes_environment_metrics.png and b/docs/assets/screenshots/nodes_environment_metrics.png differ diff --git a/docs/assets/screenshots/nodes_node_list.png b/docs/assets/screenshots/nodes_node_list.png index cc571c3b1d..5a06d5a738 100644 Binary files a/docs/assets/screenshots/nodes_node_list.png and b/docs/assets/screenshots/nodes_node_list.png differ diff --git a/docs/assets/screenshots/nodes_position.png b/docs/assets/screenshots/nodes_position.png index 5ba415233b..02546c7be5 100644 Binary files a/docs/assets/screenshots/nodes_position.png and b/docs/assets/screenshots/nodes_position.png differ diff --git a/docs/assets/screenshots/onboarding_welcome.png b/docs/assets/screenshots/onboarding_welcome.png index effb2699c1..05bad8abb5 100644 Binary files a/docs/assets/screenshots/onboarding_welcome.png and b/docs/assets/screenshots/onboarding_welcome.png differ diff --git a/docs/assets/screenshots/settings_app_info.png b/docs/assets/screenshots/settings_app_info.png index ff78aa0353..c324cc2f25 100644 Binary files a/docs/assets/screenshots/settings_app_info.png and b/docs/assets/screenshots/settings_app_info.png differ diff --git a/docs/assets/screenshots/settings_appearance.png b/docs/assets/screenshots/settings_appearance.png index aecd2b6208..ff3b997181 100644 Binary files a/docs/assets/screenshots/settings_appearance.png and b/docs/assets/screenshots/settings_appearance.png differ diff --git a/docs/assets/screenshots/settings_notifications.png b/docs/assets/screenshots/settings_notifications.png new file mode 100644 index 0000000000..a18dcf00b6 Binary files /dev/null and b/docs/assets/screenshots/settings_notifications.png differ diff --git a/docs/assets/screenshots/settings_persistence.png b/docs/assets/screenshots/settings_persistence.png index f2f5aac537..5daff81b02 100644 Binary files a/docs/assets/screenshots/settings_persistence.png and b/docs/assets/screenshots/settings_persistence.png differ diff --git a/docs/assets/screenshots/settings_slider.png b/docs/assets/screenshots/settings_slider.png index f05643897b..49ed1ba7ed 100644 Binary files a/docs/assets/screenshots/settings_slider.png and b/docs/assets/screenshots/settings_slider.png differ diff --git a/docs/assets/screenshots/settings_switch.png b/docs/assets/screenshots/settings_switch.png index c1b4557551..166d5cefc2 100644 Binary files a/docs/assets/screenshots/settings_switch.png and b/docs/assets/screenshots/settings_switch.png differ diff --git a/docs/en/developer.md b/docs/en/developer.md index 19785e5c73..7d100b6ae6 100644 --- a/docs/en/developer.md +++ b/docs/en/developer.md @@ -18,8 +18,8 @@ Things that trip up first-time contributors — check these before requesting re - **Formatting passes** — run `./gradlew spotlessApply` to auto-format, then verify with `spotlessCheck` - **Detekt passes** — run `./gradlew detekt` and fix all reported issues - **All tests pass** — run `./gradlew test allTests` (both are needed: `test` covers Android-only modules, `allTests` covers KMP) -- **Screenshot tests pass** — if you touched any Compose UI, run `./gradlew :screenshot-tests:validateFdroidDebugScreenshotTest` and update reference images if needed -- **Proto submodule unchanged** — `core/proto/` is a read-only git submodule. Never modify proto files directly +- **Screenshot tests pass** — if you touched any Compose UI, run `./gradlew :screenshot-tests:validateDebugScreenshotTest` and update reference images if needed +- **Protos are an external dependency** — protobuf models come from the `org.meshtastic:protobufs` Maven artifact (pinned in `gradle/libs.versions.toml`); change protos upstream and bump the version, never edit generated code locally - **Docs updated** — if you changed user-visible UI, update the corresponding page under `docs/user/`. The `UI & Docs Governance` CI workflow will flag the PR if you didn't. Add the `skip-docs-check` label if it genuinely isn't needed - **Previews updated** — if you changed UI composables, update the corresponding `*Previews.kt` file and screenshot tests. The governance workflow will post an advisory. Add `skip-preview-check` to dismiss - **Branch naming** — branches must start with `feat/`, `fix/`, `chore/`, `docs/`, `build/`, `ci/`, `refactor/`, `test/`, or `deps/` @@ -34,15 +34,19 @@ Things that trip up first-time contributors — check these before requesting re Keep the last 5–8 entries and trim older ones from the bottom. --> -**May 2026** — [Measurement & Formatting](developer/measurement) — New page documenting the `MetricFormatter` API, locale-aware unit conversion patterns, and how to add new measurement types. +**June 2026** — [Architecture](developer/architecture) / [Codebase](developer/codebase) — Protos migrated from the `core/proto` git submodule to the `org.meshtastic:protobufs` Maven artifact; there is no longer a local proto module to build or sync. -**May 2026** — [Testing](developer/testing) — Compose Preview Screenshot Testing (CST) integrated: `screenshot-tests/` module, `@PreviewTest` wrappers, CI validation, docs asset pipeline. +**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** — New feature modules: `feature:discovery` (mesh network discovery, #5275) and `feature:car` (Android Auto / Car App Library, google flavor only, #5633). -**May 2026** — In-app documentation system added: markdown source under `docs/user/` and `docs/developer/` is bundled as Compose Resources and rendered via `multiplatform-markdown-renderer-m3`. +**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. -**May 2026** — [Architecture](developer/architecture) — Documented KMP module layering, Navigation 3 patterns, and feature module conventions. +**June 2026** — [Persistence](developer/persistence) — FTS5 full-text message search (#5373): a `PacketFts` virtual table mirrors `Packet.messageText`, kept in sync by Room-managed triggers. -**May 2026** — [Contributing](developer/contributing) — Established docs governance CI workflow for PRs that change UI without updating docs. +**May 2026** — [Measurement & Formatting](developer/measurement) — New page documenting the `MetricFormatter` API, locale-aware unit conversion patterns, and how to add new measurement types. + +**May 2026** — [Testing](developer/testing) — Compose Preview Screenshot Testing (CST) integrated: `screenshot-tests/` module, `@PreviewTest` wrappers, CI validation, docs asset pipeline. diff --git a/docs/en/developer/architecture.md b/docs/en/developer/architecture.md index 15ef7a1248..888a0c5c1d 100644 --- a/docs/en/developer/architecture.md +++ b/docs/en/developer/architecture.md @@ -2,7 +2,7 @@ title: Architecture parent: Developer Guide nav_order: 1 -last_updated: 2026-05-29 +last_updated: 2026-06-11 aliases: - layers - module-architecture @@ -62,6 +62,8 @@ Each `feature/` module owns a vertical slice of functionality: | `feature:docs` | In-app documentation browser | | `feature:wifi-provision` | WiFi provisioning | | `feature:widget` | Android home screen widgets | +| `feature:discovery` | Mesh network discovery | +| `feature:car` | Android Auto / Car App Library — google flavor only, conditionally registered in the google `FlavorModule` | Feature modules: - Use the `meshtastic.kmp.feature` convention plugin @@ -89,9 +91,10 @@ Shared infrastructure used by all features: | `core:di` | DI utilities | | `core:network` | HTTP/serial/transport | | `core:ble` | Bluetooth LE abstractions | -| `core:proto` | Protobuf definitions | | `core:testing` | Test utilities | +Protobuf models are no longer a local module — they come from the external `org.meshtastic:protobufs` Maven artifact (pinned in `gradle/libs.versions.toml`). + ## KMP Source Sets Each module uses the standard KMP source set hierarchy: diff --git a/docs/en/developer/codebase.md b/docs/en/developer/codebase.md index 5a89df1eeb..c0851d6d4b 100644 --- a/docs/en/developer/codebase.md +++ b/docs/en/developer/codebase.md @@ -2,7 +2,7 @@ title: Codebase parent: Developer Guide nav_order: 2 -last_updated: 2026-05-20 +last_updated: 2026-06-11 aliases: - repository-layout - project-structure @@ -32,9 +32,10 @@ Meshtastic-Android/ │ ├── firmware/ │ ├── docs/ │ ├── wifi-provision/ -│ └── widget/ +│ ├── widget/ +│ ├── discovery/ +│ └── car/ ├── core/ # Core infrastructure modules (KMP) -│ ├── api/ │ ├── barcode/ │ ├── ble/ │ ├── common/ @@ -48,13 +49,14 @@ Meshtastic-Android/ │ ├── network/ │ ├── nfc/ │ ├── prefs/ -│ ├── proto/ │ ├── repository/ │ ├── resources/ │ ├── service/ │ ├── takserver/ │ ├── testing/ │ └── ui/ +├── baselineprofile/ # Baseline Profile generation for :androidApp +├── screenshot-tests/ # Compose Preview screenshot tests ├── build-logic/ # Convention plugins and build helpers │ ├── convention/ │ └── flatpak/ diff --git a/docs/en/developer/persistence.md b/docs/en/developer/persistence.md index 1d9467e157..bcdb29da36 100644 --- a/docs/en/developer/persistence.md +++ b/docs/en/developer/persistence.md @@ -2,7 +2,7 @@ title: Persistence parent: Developer Guide nav_order: 6 -last_updated: 2026-05-13 +last_updated: 2026-06-11 aliases: - room - database @@ -31,6 +31,7 @@ The primary structured data store: - Migrations managed through Room's built-in migration system - DAO interfaces live in `core:database` - Repository layer in `core:repository` provides the public API +- Full-text message search is backed by an FTS5 content table (`PacketFts`) over `Packet`, kept in sync by Room-managed triggers ### What's Stored in Room @@ -39,6 +40,7 @@ The primary structured data store: | `NodeEntity` | All known mesh nodes and their metadata | | `MyNodeEntity` | The local node's own info | | `Packet` | Message history (channel and direct), waypoints, and telemetry data | +| `PacketFts` | FTS5 virtual table mirroring `Packet.messageText` for full-text message search (Room-managed INSERT/UPDATE/DELETE triggers keep it in sync) | | `ContactSettings` | Per-contact mute and read-state | | `ReactionEntity` | Emoji reactions on messages | | `MeshLog` | Raw mesh protocol logs | diff --git a/docs/en/developer/testing.md b/docs/en/developer/testing.md index 3706a14f39..6cdc4ed579 100644 --- a/docs/en/developer/testing.md +++ b/docs/en/developer/testing.md @@ -2,7 +2,7 @@ title: Testing parent: Developer Guide nav_order: 7 -last_updated: 2026-05-13 +last_updated: 2026-06-11 aliases: - tests - unit-tests @@ -64,6 +64,21 @@ Uses Android Gradle Plugin's native screenshot testing framework: ./gradlew :screenshot-tests:copyDocsScreenshots # Copy reference images to docs pipeline ``` +### 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). + +The Macrobenchmark generator (`BaselineProfileGenerator`) and the before/after benchmark (`StartupBenchmark`) live in `baselineprofile/src/main/kotlin/org/meshtastic/baselineprofile/`. Both run on a device/emulator: + +```bash +./gradlew :androidApp:generateGoogleReleaseBaselineProfile # Generate the profile (commit the output) +./gradlew :androidApp:benchmarkGoogleReleaseBaselineProfile # Quantify the cold-start win +``` + +The generated profile is merged into `androidApp/src/google/generated/baselineProfiles/` and packaged into release builds via `androidx.profileinstaller`. + +> ⚠️ **Warning:** The journey currently covers cold start only (launch → first frame), because CI has no paired radio. Post-connection screens (node list, map, message thread) are not yet AOT-compiled; extend the journey once a fake transport or connected device is wired into the harness. + ## Test Organization ``` diff --git a/docs/en/user.md b/docs/en/user.md index 1b88fac940..f55d77468d 100644 --- a/docs/en/user.md +++ b/docs/en/user.md @@ -19,19 +19,21 @@ Documentation for using the Meshtastic Android and Desktop app. Keep the last 5–8 entries and archive older ones by removing them. --> -**May 2026** — [Translate the App](user/translate) — New page explaining how to contribute translations to the Meshtastic app via Crowdin. +**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. -**May 2026** — [Units & Locale](user/units-and-locale) — New page explaining how the app automatically adapts temperatures, distances, speeds, and times to your device's regional settings. +**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. -**May 2026** — [Signal Meter](user/signal-meter) — New page explaining how the LoRa signal quality meter works, why negative SNR values are normal, and how to interpret RSSI vs. SNR. +**June 2026** — [Messages & Channels](user/messages-and-channels) — Added full-text message search within a conversation, with a result counter and previous/next navigation. -**May 2026** — [Messages & Channels](user/messages-and-channels) — Added reactions, message actions, and delivery retry documentation. +**June 2026** — [Android Auto](user/android-auto) — New page covering Meshtastic in Android Auto. -**May 2026** — [Nodes](user/nodes) — Corrected filtering and sorting documentation to match actual app capabilities (7 sort options, 6 filter toggles). +**June 2026** — [App Functions](user/app-functions) — New page covering App Functions, which exposes app actions to the Android system AI on Google flavor builds. -**May 2026** — [Desktop App](user/desktop) — Added keyboard shortcuts table and confirmed system tray support. +**May 2026** — [Translate the App](user/translate) — New page explaining how to contribute translations to the Meshtastic app via Crowdin. -**May 2026** — [Getting Started](user/onboarding) — Added Critical Alerts permission screen and expanded permission explanations. +**May 2026** — [Units & Locale](user/units-and-locale) — New page explaining how the app automatically adapts temperatures, distances, speeds, and times to your device's regional settings. + +**May 2026** — [Signal Meter](user/signal-meter) — New page explaining how the LoRa signal quality meter works, why negative SNR values are normal, and how to interpret RSSI vs. SNR. diff --git a/docs/en/user/android-auto.md b/docs/en/user/android-auto.md new file mode 100644 index 0000000000..aeb53435d7 --- /dev/null +++ b/docs/en/user/android-auto.md @@ -0,0 +1,54 @@ +--- +title: Android Auto +parent: User Guide +nav_order: 18 +last_updated: 2026-06-11 +description: Use Meshtastic hands-free on an Android Auto head unit — read messages aloud, reply by voice, and check nodes and mesh status while driving. +aliases: + - android-auto + - car + - head-unit + - auto +--- + +# Android Auto + +Meshtastic integrates with Android Auto so you can stay in touch with your mesh while driving, without taking your hands off the wheel or your eyes off the road. + +> ⚠️ **Note:** Android Auto support is available on **Google-flavor Android builds only**. It is not included in the F-Droid build, and it is not available on Desktop or iOS. + +## Overview + +When your phone is connected to an Android Auto head unit (or the Desktop Head Unit emulator used for development), Meshtastic appears as a messaging app built with the Android Car App Library. The car interface presents a tabbed Home screen optimized for driving-safe, glanceable use: + +- **Messages** — recent conversations, with hands-free reading and replies. +- **Nodes** — the mesh node list, with a node-detail view. +- **Status** — current connection and mesh status. + +The car app does not add a new connection of its own. It uses the Meshtastic app's existing connection, node, and message state, so it reflects whatever your phone is already connected to. + +> ⚠️ **Note:** Your phone must be connected to a Meshtastic radio for the car app to show live data. If the app is disconnected, the car screen reflects that disconnected state. + +## Messages + +The Messages tab lists your recent conversations. While driving, you can: + +- **Have messages read aloud** so you don't need to look at the screen. +- **Reply by voice or text** using your head unit's reply control, dictating your response hands-free. + +## Nodes + +The Nodes tab shows your mesh node list in a car-friendly layout. Selecting a node opens a node-detail view with key information about that node. See [Nodes](nodes) for the full meaning of the information shown. + +## Status + +The Status tab summarizes your current connection and mesh status at a glance — useful for confirming you're still connected to your radio without opening your phone. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — full messaging features on your phone +- [Nodes](nodes) — detailed node list and node-detail information +- [Connections](connections) — how the app connects to your radio + +--- + diff --git a/docs/en/user/app-functions.md b/docs/en/user/app-functions.md new file mode 100644 index 0000000000..691e96e061 --- /dev/null +++ b/docs/en/user/app-functions.md @@ -0,0 +1,63 @@ +--- +title: App Functions +parent: User Guide +nav_order: 19 +last_updated: 2026-06-11 +description: Expose mesh capabilities to the Android system and on-device AI assistants (e.g. Gemini) so they can run mesh workflows without opening the app. +aliases: + - app-functions + - system-ai + - gemini + - assistant-functions +--- + +# App Functions + +App Functions expose Meshtastic capabilities to the Android system and to on-device AI assistants (such as Gemini) through the Android App Functions API. With them enabled, an assistant can discover and trigger mesh workflows for you — for example sending a message or checking your mesh status — without you opening the app. + +> ⚠️ **Note:** App Functions are available on **Google-flavor Android builds only**. + +> ⚠️ **Note:** This is separate from the in-app **Chirpy** assistant. App Functions let the *system* AI assistant act on your mesh; Chirpy is a conversational assistant inside the Meshtastic app itself. + +## Enabling App Functions + +App Functions are controlled from **Settings → System AI** (the in-app screen is labeled "System AI"). The screen has: + +- A **master toggle** labeled **"Allow AI access"**, with the subtitle *"Let system AI assistants (e.g. Gemini) discover and use mesh functions"*. When off, no functions are exposed to the system. +- An **individual toggle for each function**, so you can expose only the capabilities you want. + +The functions are grouped into a **Write** section (functions that change something or send data to your mesh) and a **Read** section (functions that only return information). + +![App Functions screen with master and per-function toggles](../../assets/screenshots/app-functions_settings.png) + +### Write Functions + +| Function | What it does | +|----------|--------------| +| **Send Message** | Sends a text message to a contact (direct message) or to a channel, up to 237 bytes. | + +### Read Functions + +| Function | What it returns | +|----------|-----------------| +| **Get Mesh Status** | Overall mesh status. | +| **Get Node List** | The list of nodes on your mesh. | +| **Get Channel Info** | Information about your channels. | +| **Get Device Status** | Status of your connected radio. | +| **Get Node Details** | Detailed information about a specific node. | +| **Get Recent Messages** | Recent messages from your conversations. | +| **Get Unread Summary** | A summary of unread messages. | +| **Get Mesh Metrics** | Telemetry and metrics from your mesh. | + +## Privacy + +> 🔒 **Privacy:** The **Send Message** function lets an assistant send messages to your mesh on your behalf. Only enable functions you trust the assistant to use. The read functions expose node, message, and metric data to the assistant — enable only what you're comfortable sharing. Each function has its own toggle, and the master toggle turns all of them off at once. + +## Related Topics + +- [Messages & Channels](messages-and-channels) — sending messages directly in the app +- [Nodes](nodes) — the node list the read functions draw from +- [Node Metrics](node-metrics) — the telemetry behind Get Mesh Metrics + +--- + diff --git a/docs/en/user/desktop.md b/docs/en/user/desktop.md index 20c0754816..2daa59aeb5 100644 --- a/docs/en/user/desktop.md +++ b/docs/en/user/desktop.md @@ -2,7 +2,7 @@ title: Desktop App parent: User Guide nav_order: 14 -last_updated: 2026-05-20 +last_updated: 2026-06-11 description: Install and use the Meshtastic Desktop app on Linux, macOS, and Windows — connections, feature parity, and keyboard shortcuts. aliases: - desktop @@ -70,10 +70,14 @@ Bluetooth Low Energy is supported on Desktop via the [Kable](https://github.com/ | Firmware Update OTA | ✓ | ✗ | Use web flasher | | Notifications | ✓ | ✓ | Native OS notifications | | Widgets | ✓ | ✗ | Android-only | +| Android Auto | ✓ | ✗ | Android-only — not available on Desktop or iOS | | AI Assistant (Chirpy) | ✓* | ✗ | Google flavor Android only | +| App Functions (system AI) | ✓† | ✗ | Google flavor Android only | *Chirpy AI requires Android 14+ on Google flavor builds with supported hardware. +†App Functions exposes app actions to the Android system AI on Google flavor builds. See [App Functions](app-functions). + ## UI Differences The Desktop app uses the same Compose Multiplatform UI with adaptations for larger screens and desktop interaction. diff --git a/docs/en/user/discovery.md b/docs/en/user/discovery.md index 3ce9078d6a..78ad7c9506 100644 --- a/docs/en/user/discovery.md +++ b/docs/en/user/discovery.md @@ -2,8 +2,8 @@ title: Discovery parent: User Guide nav_order: 12 -last_updated: 2026-05-13 -description: Explore your mesh network — traceroute paths, neighbor maps, and node discovery tools. +last_updated: 2026-06-11 +description: Explore your mesh network — the Local Mesh Discovery scanner, traceroute paths, neighbor maps, and node discovery tools. aliases: - mesh-discovery - local-discovery @@ -16,10 +16,83 @@ aliases: Discovery tools help you understand **how** your mesh network is connected — which nodes can hear each other, what paths messages take, and where bottlenecks or weak links exist. -> 💡 **Tip:** You don't need a dedicated "discovery mode" to start exploring your mesh. The tools below are available right now from the node list and node detail screens. +The app offers two complementary approaches: + +- **Local Mesh Discovery (Scanner)** — an automated mode that cycles your connected radio through different LoRa presets, listens on each, and ranks which preset performs best at your location. +- **Manual exploration** — traceroute, Neighbor Info, and the node list, which you can use at any time to investigate specific paths and topology. --- +## Local Mesh Discovery (Scanner) + +Local Mesh Discovery is a dedicated scanning mode that helps you find the best LoRa modem preset for your location and see which nodes are active on each preset. It cycles your connected radio through one or more presets you choose, listens (or "dwells") on each one for a set time to collect packets, then analyzes and ranks the results. + +Open it from **Settings → Local Mesh Discovery**. + +> ⚠️ **Note:** Discovery temporarily changes your radio's LoRa settings while it scans, then restores your original configuration when it finishes. Your device must be connected to run a scan. + +### Setting Up a Scan + +Before starting, configure these controls: + +| Control | Description | +|---------|-------------| +| **LoRa preset picker** | Select one or more presets to scan. Discovery dwells on each selected preset in turn. | +| **Dwell time** | Time to listen on each preset. Choose from 1, 5, 15, 30, 45, 60, 90, 120, or 180 minutes. Longer dwell times collect more packets and give a clearer picture, but take longer. | +| **Keep screen awake** | Optional toggle that prevents the screen from sleeping during a long scan. | + +The **Start** button stays disabled — with an explanation of why — until the scan can run. Common reasons it's disabled: + +- The device is **not connected**. +- The current channel is using the **default channel key** (use a unique key first — see [Messages & Channels](messages-and-channels)). +- **No presets** have been selected to scan. +- The selected preset uses **2.4 GHz**, which your hardware doesn't support. + +### Live Progress + +While a scan runs, Discovery shows its current stage: + +| Stage | What's happening | +|-------|------------------| +| **Preparing** | Saving your current configuration and getting ready to scan. | +| **Shifting to \** | Switching the radio to the next preset to test. | +| **Reconnecting** | Re-establishing the connection after the preset change. | +| **Dwell** | Listening on the current preset to collect packets, with a countdown to the next step. | +| **Analysis** | Processing the collected packets and ranking the presets. | +| **Restoring** | Putting your original LoRa configuration back. | + +![Dwell countdown showing time remaining on the current preset](../../assets/screenshots/discovery_dwell_progress.png) + +### Reading the Results + +When the scan completes, Discovery presents a per-preset result card for each preset it tested, plus an overall summary. + +![Per-preset result card with ranking and collected metrics](../../assets/screenshots/discovery_preset_result.png) + +Metrics include: + +| Metric | What it tells you | +|--------|-------------------| +| RF health | Overall quality of the radio environment on that preset. | +| Channel utilization | How busy the airwaves were during the dwell. | +| Airtime | Transmission time observed. | +| Direct vs. relayed nodes | How many mesh nodes were heard directly versus via a relay. | +| Bad / duplicate packets | Counts of corrupt and repeated packets, indicating congestion or interference. | + +Additional features available from the results: + +- **Scan History** — saved sessions you can revisit; view or delete past scans. +- **Discovery Map** — a map of the nodes found during the scan. +- **Report export** — export a report as a PDF on Android, or as text on other platforms. + +> 💡 **Tip:** On Android, Discovery can generate an on-device AI summary (Gemini Nano) of your results. If the on-device model isn't available, an algorithmic summary is used instead — so you always get a readable interpretation of the scan. + +--- + +## Manual Exploration + +The tools below are available at any time from the node list and node detail screens. Use them to investigate specific paths and build a topology picture, alongside or instead of a full scan. + ## Traceroute Traceroute reveals the exact path a message takes from your node to any other node on the mesh. It's the single most useful tool for debugging connectivity problems. diff --git a/docs/en/user/messages-and-channels.md b/docs/en/user/messages-and-channels.md index a01cc0d374..cdfeaee2ce 100644 --- a/docs/en/user/messages-and-channels.md +++ b/docs/en/user/messages-and-channels.md @@ -2,8 +2,8 @@ title: Messages & Channels parent: User Guide nav_order: 3 -last_updated: 2026-05-13 -description: Send and receive messages, manage channels, configure encryption, and use quick chat, reactions, and message actions. +last_updated: 2026-06-11 +description: Send and receive messages, manage channels, configure encryption, search conversations, and use quick chat, reactions, and message actions. aliases: - channels - direct-messages @@ -100,6 +100,19 @@ Pre-configured messages for rapid communication: The channel list shows each channel with its latest message preview. +### Searching Messages + +You can search the full history of any conversation directly from the chat screen: + +1. Open a conversation (a channel or a direct message). +2. Tap the **search icon** in the top bar. +3. Type into the **Search messages…** field. The search runs as you type, across all stored messages in that conversation. +4. Use the **N / M** result counter and the **previous / next arrows** to jump between matches, which are highlighted in the conversation. + +![Message search bar with result counter and previous/next arrows](../../assets/screenshots/messages_search_bar.png) + +> 💡 **Tip:** Search is full-text and stays within the conversation you opened it from — it doesn't search across other channels or contacts. Matching is fast even on long histories because messages are indexed locally. + ### Message Bubbles Messages appear as chat bubbles — sent messages on the right, received messages on the left. Each bubble shows the sender, timestamp, and delivery status. Messages with replies include a quoted preview of the original message above the response. diff --git a/docs/en/user/node-metrics.md b/docs/en/user/node-metrics.md index e9738256cc..a3f2884100 100644 --- a/docs/en/user/node-metrics.md +++ b/docs/en/user/node-metrics.md @@ -2,8 +2,8 @@ title: Node Metrics parent: User Guide nav_order: 5 -last_updated: 2026-05-13 -description: Telemetry dashboards for each mesh node — device health, environment sensors, signal quality, power, traceroute, and position history. +last_updated: 2026-06-11 +description: Telemetry dashboards for each mesh node — device health, environment sensors, air quality, signal quality, power, traceroute, and position history. aliases: - metrics - telemetry @@ -47,6 +47,38 @@ Environment metrics are charted over time for easy trend analysis — temperatur > 💡 **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 + +Air Quality is a dedicated metrics view for nodes equipped with a particulate-matter and/or CO₂ sensor. It is **separate from the BME680 IAQ reading** listed under Environment Metrics — IAQ is a single gas-resistance-derived index, while the Air Quality view charts the underlying particulate and CO₂ measurements. + +| Metric | Unit | Description | +|--------|------|-------------| +| PM1.0 | µg/m³ | Particulate matter up to 1.0 micron | +| PM2.5 | µg/m³ | Particulate matter up to 2.5 microns | +| PM10 | µg/m³ | Particulate matter up to 10 microns | +| CO₂ | ppm | Carbon dioxide concentration | + +CO₂ readings are color-coded by severity to make air quality easy to read at a glance: + +| Band | CO₂ Range (ppm) | Color | +|------|-----------------|-------| +| Good | < 1000 | Green | +| Stuffy | < 2000 | Amber | +| Poor | < 5000 | Orange | +| Unsafe | < 30000 | Red | +| Evacuate | ≥ 30000 | Dark red | + +![Air quality readings with color-coded CO₂ severity](../../assets/screenshots/node-metrics_air_quality.png) + +An air-quality log/metrics button appears on the node detail screen **only when the node has reported air-quality telemetry**. From the Air Quality view you can: + +- Select a **time frame** for the charts. +- Filter with **metric chips** — only metrics that have data are shown. +- **Refresh / request** the latest air-quality telemetry. +- **Export to CSV** for analysis in a spreadsheet. + +> 💡 **Tip:** Air Quality metrics require a compatible air-quality sensor on the remote node. If a node has no particulate or CO₂ sensor, the air-quality button won't appear. See [Telemetry & Sensors](telemetry-and-sensors) for supported hardware. + ## Signal Metrics Radio signal quality information: diff --git a/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/component/DiscoveryPreviews.kt b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/component/DiscoveryPreviews.kt new file mode 100644 index 0000000000..53319951e0 --- /dev/null +++ b/feature/discovery/src/commonMain/kotlin/org/meshtastic/feature/discovery/ui/component/DiscoveryPreviews.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.discovery.ui.component + +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.PreviewLightDark +import org.meshtastic.core.database.entity.DiscoveryPresetResultEntity +import org.meshtastic.core.ui.theme.AppTheme + +@PreviewLightDark +@Suppress("MagicNumber", "PreviewPublic") // fake data; public so :screenshot-tests can reference it +@Composable +fun PreviewDiscoveryPresetResult() { + AppTheme { + Surface { + PresetResultCard( + result = + DiscoveryPresetResultEntity( + sessionId = 1, + presetName = "LongFast", + dwellDurationSeconds = 900, + uniqueNodes = 12, + directNeighborCount = 5, + meshNeighborCount = 7, + infrastructureNodeCount = 2, + messageCount = 34, + sensorPacketCount = 18, + avgChannelUtilization = 14.2, + avgAirtimeRate = 3.1, + packetSuccessRate = 96.5, + packetFailureRate = 3.5, + numPacketsTx = 21, + numPacketsRx = 412, + numPacketsRxBad = 6, + numRxDupe = 11, + numTxRelay = 38, + numOnlineNodes = 12, + numTotalNodes = 40, + ), + nodes = emptyList(), + rank = 1, + ) + } + } +} + +@PreviewLightDark +@Suppress("MagicNumber", "PreviewPublic") // fake data; public so :screenshot-tests can reference it +@Composable +fun PreviewDiscoveryDwellProgress() { + AppTheme { Surface { DwellProgressIndicator(presetName = "LongFast", remainingSeconds = 312, totalSeconds = 900) } } +} diff --git a/feature/docs/build.gradle.kts b/feature/docs/build.gradle.kts index 6899a19a4d..b35fcc76f5 100644 --- a/feature/docs/build.gradle.kts +++ b/feature/docs/build.gradle.kts @@ -63,7 +63,7 @@ val syncDocsToComposeResources by group = "docs" val docsEnDir = rootProject.layout.projectDirectory.dir("docs/en") - val screenshotsDir = rootProject.layout.projectDirectory.dir("docs/screenshots") + val screenshotsDir = rootProject.layout.projectDirectory.dir("docs/assets/screenshots") val composeResourcesTarget = layout.projectDirectory.dir("src/commonMain/composeResources/files/docs") from(docsEnDir) { @@ -72,9 +72,9 @@ val syncDocsToComposeResources by } // FR-038: Bundle screenshots into assets/screenshots/ to match markdown image paths. - // Markdown references use `assets/screenshots/foo.png` (relative to the doc page). - // copyDocsScreenshots flattens reference PNGs into docs/screenshots/, - // so we remap them into assets/screenshots/ within the compose resource tree. + // docs/assets/screenshots/ is the tracked, semantically-named screenshot set shared with the + // Jekyll site; copyDocsScreenshots refreshes it from the CST reference images, so the site and + // the in-app reader always serve identical images (and raw CST renders never reach the bundle). from(screenshotsDir) { include("**/*.png") into("assets/screenshots") diff --git a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/data/DocBundleLoader.kt b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/data/DocBundleLoader.kt index 9ea02dbf48..7798895bb9 100644 --- a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/data/DocBundleLoader.kt +++ b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/data/DocBundleLoader.kt @@ -21,6 +21,8 @@ import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.getString import org.koin.core.annotation.Single import org.meshtastic.core.common.util.currentLocaleQualifier +import org.meshtastic.core.resources.doc_keywords_android_auto +import org.meshtastic.core.resources.doc_keywords_app_functions import org.meshtastic.core.resources.doc_keywords_connections import org.meshtastic.core.resources.doc_keywords_desktop import org.meshtastic.core.resources.doc_keywords_discovery @@ -38,6 +40,8 @@ import org.meshtastic.core.resources.doc_keywords_tak import org.meshtastic.core.resources.doc_keywords_telemetry import org.meshtastic.core.resources.doc_keywords_translate import org.meshtastic.core.resources.doc_keywords_units +import org.meshtastic.core.resources.doc_title_android_auto +import org.meshtastic.core.resources.doc_title_app_functions import org.meshtastic.core.resources.doc_title_connections import org.meshtastic.core.resources.doc_title_desktop import org.meshtastic.core.resources.doc_title_discovery @@ -407,6 +411,26 @@ class DefaultDocBundleLoader : DocBundleLoader { 3700, "translate", ), + UserPageDef( + "android-auto", + CoreRes.string.doc_title_android_auto, + CoreRes.string.doc_keywords_android_auto, + "en/user/android-auto.html", + 18, + listOf("android-auto", "car", "head-unit", "auto"), + 2119, + "android-auto", + ), + UserPageDef( + "app-functions", + CoreRes.string.doc_title_app_functions, + CoreRes.string.doc_keywords_app_functions, + "en/user/app-functions.html", + 19, + listOf("app-functions", "system-ai", "gemini", "assistant"), + 2750, + "app-functions", + ), ) private suspend fun buildUserGuideIndex(): List = userPages.map { def -> diff --git a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/ComposeResourceImageTransformer.kt b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/ComposeResourceImageTransformer.kt index 31bfd82b35..0ad48ad7e2 100644 --- a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/ComposeResourceImageTransformer.kt +++ b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/ComposeResourceImageTransformer.kt @@ -33,8 +33,30 @@ import com.mikepenz.markdown.model.ImageTransformer import meshtasticandroid.feature.docs.generated.resources.Res import org.jetbrains.compose.resources.MissingResourceException +private const val ASSETS_SEGMENT = "assets/" + +/** + * Maps a markdown image link to its bundled compose resource path, or `null` for external URLs. + * + * Authored pages use paths relative to the Jekyll source layout (`../../assets/screenshots/foo.png` from + * `docs/en/user/page.md`), while the compose resource tree drops the `en/` level (`files/docs/user/page.md` with + * screenshots at `files/docs/assets/screenshots/`). Relative prefixes therefore cannot be resolved literally; instead, + * anything from the `assets/` segment onward is anchored at `files/docs/`, which matches where + * `syncDocsToComposeResources` places the bundled screenshots. + */ +internal fun resolveDocImageResourcePath(link: String): String? { + if (link.startsWith("http://") || link.startsWith("https://")) return null + val assetsIndex = link.indexOf(ASSETS_SEGMENT) + val isSegmentStart = assetsIndex == 0 || (assetsIndex > 0 && link[assetsIndex - 1] == '/') + return if (assetsIndex >= 0 && isSegmentStart) { + "files/docs/${link.substring(assetsIndex)}" + } else { + "files/docs/${link.removePrefix("/")}" + } +} + /** - * Resolves local markdown image references (e.g. `assets/screenshots/foo.png`) to bundled Compose resources via + * Resolves local markdown image references (e.g. `../../assets/screenshots/foo.png`) to bundled Compose resources via * [Res.getUri] and loads them asynchronously using Coil 3's [rememberAsyncImagePainter]. * * External URLs (`http://` / `https://`) return `null` so the default renderer behaviour applies (or they are simply @@ -42,18 +64,14 @@ import org.jetbrains.compose.resources.MissingResourceException * not yet been generated or synced. * * FR-038: Screenshots synced by `syncDocsToComposeResources` land under - * `composeResources/files/docs/assets/screenshots/`, matching the relative paths used in the authored markdown. + * `composeResources/files/docs/assets/screenshots/`; [resolveDocImageResourcePath] maps the authored markdown paths + * onto that location. */ class ComposeResourceImageTransformer : ImageTransformer { @Composable override fun transform(link: String): ImageData? { - if (link.startsWith("http://") || link.startsWith("https://")) return null - - // Markdown uses root-relative paths (/assets/screenshots/foo.png) for Jekyll compatibility. - // Strip the leading slash to build the compose resource path. - val relativePath = link.removePrefix("/") - val resourcePath = "files/docs/$relativePath" + val resourcePath = resolveDocImageResourcePath(link) ?: return null val uri = try { Res.getUri(resourcePath) diff --git a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocPageIconResolver.kt b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocPageIconResolver.kt index b2877bd53a..d7dd2fd24c 100644 --- a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocPageIconResolver.kt +++ b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/ui/DocPageIconResolver.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.vector.ImageVector import org.meshtastic.core.ui.icon.Altitude import org.meshtastic.core.ui.icon.Antenna +import org.meshtastic.core.ui.icon.Api import org.meshtastic.core.ui.icon.BluetoothConnected import org.meshtastic.core.ui.icon.BugReport import org.meshtastic.core.ui.icon.Chart @@ -34,6 +35,7 @@ import org.meshtastic.core.ui.icon.Nodes import org.meshtastic.core.ui.icon.Notes import org.meshtastic.core.ui.icon.PersonSearch import org.meshtastic.core.ui.icon.PinDrop +import org.meshtastic.core.ui.icon.Route import org.meshtastic.core.ui.icon.Rssi import org.meshtastic.core.ui.icon.Settings import org.meshtastic.core.ui.icon.SignalCellular3Bar @@ -79,6 +81,10 @@ internal fun DocPage.resolveIcon(): ImageVector = when (iconId) { "translate" -> MeshtasticIcons.Language + "android-auto" -> MeshtasticIcons.Route + + "app-functions" -> MeshtasticIcons.Api + // Developer Guide "architecture" -> MeshtasticIcons.ForkLeft diff --git a/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/DocImageWiringTest.kt b/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/DocImageWiringTest.kt new file mode 100644 index 0000000000..d8a1a0ff33 --- /dev/null +++ b/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/DocImageWiringTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.docs + +import kotlinx.coroutines.test.runTest +import meshtasticandroid.feature.docs.generated.resources.Res +import org.meshtastic.feature.docs.data.DefaultDocBundleLoader +import org.meshtastic.feature.docs.ui.resolveDocImageResourcePath +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue + +/** + * Guards the docs image pipeline end to end: every image referenced by a bundled markdown page must resolve (via the + * same path mapping the in-app renderer uses) to a real bundled compose resource. Catches broken aliases, missing + * screenshot assets, and regressions in the markdown-path-to-resource mapping. + */ +class DocImageWiringTest { + + private val loader = DefaultDocBundleLoader() + + @Test + fun `relative asset links resolve to the bundled screenshots directory`() { + assertEquals( + "files/docs/assets/screenshots/nodes_node_list.png", + resolveDocImageResourcePath("../../assets/screenshots/nodes_node_list.png"), + ) + } + + @Test + fun `root-relative asset links resolve to the bundled screenshots directory`() { + assertEquals( + "files/docs/assets/screenshots/foo.png", + resolveDocImageResourcePath("/assets/screenshots/foo.png"), + ) + } + + @Test + fun `external links are not transformed`() { + assertNull(resolveDocImageResourcePath("https://example.com/x.png")) + assertNull(resolveDocImageResourcePath("http://example.com/x.png")) + } + + @Test + fun `every image referenced by a bundled page resolves to a bundled resource`() = runTest { + val bundle = loader.load() + val imagePattern = Regex("""!\[[^\]]*]\(([^)\s]+)\)""") + val missing = mutableListOf() + for (page in bundle.pages) { + val markdown = loader.readPage(page.id)?.markdown ?: continue + for (match in imagePattern.findAll(markdown)) { + val link = match.groupValues[1] + val resourcePath = resolveDocImageResourcePath(link) ?: continue + val exists = runCatching { Res.readBytes(resourcePath).isNotEmpty() }.getOrDefault(false) + if (!exists) missing += "${page.id}: $link -> $resourcePath" + } + } + assertTrue(missing.isEmpty(), "Unresolvable doc images:\n${missing.joinToString("\n")}") + } +} diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageSearchBarPreviews.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageSearchBarPreviews.kt new file mode 100644 index 0000000000..b5b3805b17 --- /dev/null +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageSearchBarPreviews.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.messaging.component + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.PreviewLightDark +import org.meshtastic.core.ui.theme.AppTheme + +@PreviewLightDark +@Suppress("PreviewPublic") // public so :screenshot-tests can reference it +@Composable +fun MessageSearchBarPreview() { + AppTheme { MessageSearchBar(query = "solar", onQueryChange = {}, onClose = {}, resultCount = 4, currentIndex = 1) } +} diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/AirQualityMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/AirQualityMetrics.kt index bf97e8a144..ad46dd396c 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/AirQualityMetrics.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/AirQualityMetrics.kt @@ -32,6 +32,7 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.rememberScrollState import androidx.compose.material3.FilterChip import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -43,6 +44,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.patrykandpatrick.vico.compose.cartesian.VicoScrollState @@ -62,12 +64,14 @@ import org.meshtastic.core.resources.pm10 import org.meshtastic.core.resources.pm1_0 import org.meshtastic.core.resources.pm2_5 import org.meshtastic.core.ui.component.Co2Severity +import org.meshtastic.core.ui.theme.AppTheme import org.meshtastic.core.ui.theme.GraphColors.Blue import org.meshtastic.core.ui.theme.GraphColors.Cyan import org.meshtastic.core.ui.theme.GraphColors.Green import org.meshtastic.core.ui.theme.GraphColors.Red import org.meshtastic.core.ui.util.rememberSaveFileLauncher import org.meshtastic.proto.Telemetry +import org.meshtastic.proto.AirQualityMetrics as AirQualityMetricsProto /** Selectable chart metric enum for air quality data series. */ private enum class AirQuality(val labelRes: StringResource, val unit: String, val color: Color) { @@ -258,41 +262,88 @@ private fun AirQualityChart( } @Composable -private fun AirQualityMetricsCard(telemetry: Telemetry, isSelected: Boolean, onClick: () -> Unit) { +private fun AirQualityMetricsCard( + telemetry: Telemetry, + isSelected: Boolean, + onClick: () -> Unit, + timeTextOverride: String? = null, +) { val aq = telemetry.air_quality_metrics ?: return - val time = DateFormatter.formatDateTime(telemetry.time.toLong() * MS_PER_SEC) + val time = timeTextOverride ?: DateFormatter.formatDateTime(telemetry.time.toLong() * MS_PER_SEC) SelectableMetricCard(isSelected = isSelected, onClick = onClick) { - Text( - text = time, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Spacer(modifier = Modifier.height(4.dp)) - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Column { - aq.pm10_standard - ?.takeIf { it != 0 } - ?.let { Text("PM1.0: $it µg/m³", style = MaterialTheme.typography.bodySmall) } - aq.pm25_standard - ?.takeIf { it != 0 } - ?.let { Text("PM2.5: $it µg/m³", style = MaterialTheme.typography.bodySmall) } - aq.pm100_standard - ?.takeIf { it != 0 } - ?.let { Text("PM10: $it µg/m³", style = MaterialTheme.typography.bodySmall) } + // SelectableMetricCard's SelectionContainer imposes no layout of its own, + // so the card content must bring its own Column (matches EnvironmentMetricsContent). + Column(modifier = Modifier.fillMaxWidth().padding(12.dp)) { + Text( + text = time, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(modifier = Modifier.height(4.dp)) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Column { + aq.pm10_standard + ?.takeIf { it != 0 } + ?.let { Text("PM1.0: $it µg/m³", style = MaterialTheme.typography.bodySmall) } + aq.pm25_standard + ?.takeIf { it != 0 } + ?.let { Text("PM2.5: $it µg/m³", style = MaterialTheme.typography.bodySmall) } + aq.pm100_standard + ?.takeIf { it != 0 } + ?.let { Text("PM10: $it µg/m³", style = MaterialTheme.typography.bodySmall) } + } + Column { + aq.co2 + ?.takeIf { it != 0 } + ?.let { co2 -> + val severity = Co2Severity.fromPpm(co2) + Text( + text = "CO₂: $co2 ppm", + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Medium, + color = severity?.color ?: MaterialTheme.colorScheme.onSurface, + ) + } + } } + } + } +} + +@PreviewLightDark +@Suppress("MagicNumber", "PreviewPublic") // fake data; public so :screenshot-tests can reference it +@Composable +fun PreviewAirQualityCards() { + val readings = + listOf( + Telemetry( + time = 1700000000, + air_quality_metrics = + AirQualityMetricsProto(pm10_standard = 4, pm25_standard = 9, pm100_standard = 12, co2 = 620), + ) to "2023-11-14 20:13", + Telemetry( + time = 1700003600, + air_quality_metrics = + AirQualityMetricsProto(pm10_standard = 6, pm25_standard = 14, pm100_standard = 19, co2 = 1450), + ) to "2023-11-14 21:13", + Telemetry( + time = 1700007200, + air_quality_metrics = + AirQualityMetricsProto(pm10_standard = 11, pm25_standard = 25, pm100_standard = 33, co2 = 2300), + ) to "2023-11-14 22:13", + ) + AppTheme { + Surface { Column { - aq.co2 - ?.takeIf { it != 0 } - ?.let { co2 -> - val severity = Co2Severity.fromPpm(co2) - Text( - text = "CO₂: $co2 ppm", - style = MaterialTheme.typography.bodySmall, - fontWeight = FontWeight.Medium, - color = severity?.color ?: MaterialTheme.colorScheme.onSurface, - ) - } + readings.forEach { (telemetry, timeText) -> + AirQualityMetricsCard( + telemetry = telemetry, + isSelected = false, + onClick = {}, + timeTextOverride = timeText, + ) + } } } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/appfunctions/AppFunctionsSettingsScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/appfunctions/AppFunctionsSettingsScreen.kt index 368150c088..79b6487fc2 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/appfunctions/AppFunctionsSettingsScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/appfunctions/AppFunctionsSettingsScreen.kt @@ -23,10 +23,12 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource @@ -49,6 +51,7 @@ import org.meshtastic.core.ui.component.MainAppBar import org.meshtastic.core.ui.component.SwitchListItem import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.SettingsRemote +import org.meshtastic.core.ui.theme.AppTheme @Composable fun AppFunctionsSettingsScreen( @@ -224,3 +227,38 @@ private fun ReadFunctionsSection( onClick = onToggleUnreadSummary, ) } + +@PreviewLightDark +@Suppress("PreviewPublic") // public so :screenshot-tests can reference it +@Composable +fun PreviewAppFunctionsSettings() { + AppTheme { + Surface { + Column { + MasterToggleSection(masterEnabled = true, onToggle = {}) + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + WriteFunctionsSection(masterEnabled = true, sendMessage = false, onToggleSendMessage = {}) + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + ReadFunctionsSection( + masterEnabled = true, + getMeshStatus = true, + onToggleMeshStatus = {}, + getNodeList = true, + onToggleNodeList = {}, + getChannelInfo = true, + onToggleChannelInfo = {}, + getDeviceStatus = true, + onToggleDeviceStatus = {}, + getNodeDetails = true, + onToggleNodeDetails = {}, + getMeshMetrics = true, + onToggleMeshMetrics = {}, + getRecentMessages = false, + onToggleRecentMessages = {}, + getUnreadSummary = true, + onToggleUnreadSummary = {}, + ) + } + } + } +} diff --git a/screenshot-tests/build.gradle.kts b/screenshot-tests/build.gradle.kts index 964b3891c3..de011c43ea 100644 --- a/screenshot-tests/build.gradle.kts +++ b/screenshot-tests/build.gradle.kts @@ -52,6 +52,7 @@ dependencies { implementation(project(":feature:intro")) implementation(project(":feature:map")) implementation(project(":feature:docs")) + implementation(project(":feature:discovery")) implementation(libs.compose.multiplatform.foundation) implementation(libs.compose.multiplatform.material3) @@ -62,13 +63,14 @@ dependencies { } tasks.register("copyDocsScreenshots") { - description = "Copies selected reference screenshots to docs/screenshots/ for the docs pipeline." + description = + "Refreshes the semantically-named docs screenshots in docs/assets/screenshots/ from CST reference images." group = "documentation" val referenceDir = layout.projectDirectory.dir("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/screenshots") + val outputDir = rootProject.layout.projectDirectory.dir("docs/assets/screenshots") // Read manifest patterns at configuration time so Copy task can resolve includes val manifestPatterns = @@ -97,10 +99,16 @@ tasks.register("copyDocsScreenshots") { } } - // Flatten directory structure and apply alias renaming + // Flatten directory structure, keep only screenshots with a semantic alias, and rename them. + // Unaliased reference images are skipped: only curated, semantically-named screenshots feed the + // docs site and the in-app bundle. eachFile { val alias = reverseAliases[name] - path = alias ?: name + if (alias == null) { + exclude() + } else { + path = alias + } } duplicatesStrategy = DuplicatesStrategy.WARN includeEmptyDirs = false diff --git a/screenshot-tests/docs-screenshot-aliases.properties b/screenshot-tests/docs-screenshot-aliases.properties index bf1353a9ad..ea02e6db9a 100644 --- a/screenshot-tests/docs-screenshot-aliases.properties +++ b/screenshot-tests/docs-screenshot-aliases.properties @@ -1,6 +1,8 @@ # Screenshot alias mapping: semantic_name=CST_reference_filename # Used by copyDocsScreenshots to rename CST-generated screenshots to the # human-readable names referenced in docs/user/**/*.md and docs/developer/**/*.md. +# Only aliased screenshots are copied into docs/assets/screenshots/ (the tracked +# set shared by the Jekyll site and the in-app docs bundle). # # Format: docs_name=cst_reference_filename (both without directory prefix) # Lines starting with # are comments. Blank lines are ignored. @@ -12,22 +14,29 @@ onboarding_welcome.png=ScreenshotWelcomeScreen_Light_b29dc7a7_0.png connections_bluetooth_scan.png=ScreenshotScanningBle_Light_b29dc7a7_0.png connections_transport_filters.png=ScreenshotTransportFilterChips_Light_b29dc7a7_0.png connections_connecting.png=ScreenshotConnectingDeviceInfo_Light_b29dc7a7_0.png +connections_disconnect.png=ScreenshotDisconnectButton_Light_b29dc7a7_0.png connections_empty_state.png=ScreenshotEmptyStateContent_Light_b29dc7a7_0.png # Firmware firmware_checking.png=ScreenshotFirmwareChecking_Light_b29dc7a7_0.png firmware_disclaimer.png=ScreenshotFirmwareDisclaimer_Light_b29dc7a7_0.png +firmware_error.png=ScreenshotFirmwareError_Light_b29dc7a7_0.png firmware_success.png=ScreenshotFirmwareSuccess_Light_b29dc7a7_0.png # Messages -messages_quick_chat.png=ScreenshotChannelInfo_Light_b29dc7a7_0.png +messages_quick_chat.png=ScreenshotQuickChatItem_Light_b29dc7a7_0.png +messages_reaction.png=ScreenshotReactionItem_Light_b29dc7a7_0.png +messages_search_bar.png=ScreenshotMessageSearchBar_Light_b29dc7a7_0.png messages-and-channels_channel_list.png=ScreenshotChannelItem_Light_b29dc7a7_0.png # Nodes nodes_node_list.png=ScreenshotNodeChip_Light_b29dc7a7_0.png -nodes_detail_section.png=ScreenshotAppInfoSection_Light_b29dc7a7_0.png -nodes_detail_local.png=ScreenshotDeviceListItem_Light_b29dc7a7_0.png -nodes_position.png=ScreenshotSatelliteCountInfo_Light_b29dc7a7_0.png +nodes_detail_section.png=ScreenshotNodeDetailsSection_Light_b29dc7a7_0.png +nodes_detail_local.png=ScreenshotNodeDetailContentLocal_Light_b29dc7a7_0.png +nodes_detail_minimal.png=ScreenshotNodeDetailContentMinimal_Light_b29dc7a7_0.png +nodes_device_metrics_card.png=ScreenshotDeviceMetricsCard_Light_b29dc7a7_0.png +nodes_environment_metrics.png=ScreenshotEnvironmentMetricsContent_Light_b29dc7a7_0.png +nodes_position.png=ScreenshotPositionInlineContent_Light_b29dc7a7_0.png nodes_signal_info.png=ScreenshotSignalInfoSimple_Light_b29dc7a7_0.png nodes_battery_info.png=ScreenshotMaterialBatteryInfo_Light_b29dc7a7_0.png nodes_hops_info.png=ScreenshotHopsInfo_Light_b29dc7a7_0.png @@ -35,7 +44,12 @@ nodes_last_heard.png=ScreenshotLastHeardInfo_Light_b29dc7a7_0.png nodes_distance_info.png=ScreenshotDistanceInfo_Light_b29dc7a7_0.png # Node metrics -node-metrics_telemetric_actions.png=ScreenshotElevationInfo_Light_b29dc7a7_0.png +node-metrics_telemetric_actions.png=ScreenshotTelemetricActionsSection_Light_b29dc7a7_0.png +node-metrics_air_quality.png=ScreenshotAirQualityCards_Light_b29dc7a7_0.png + +# Discovery (Local Mesh Discovery scanner) +discovery_preset_result.png=ScreenshotDiscoveryPresetResult_Light_b29dc7a7_0.png +discovery_dwell_progress.png=ScreenshotDiscoveryDwellProgress_Light_b29dc7a7_0.png # Settings settings-radio-user_lora_config.png=ScreenshotDropDownPreference_Light_b29dc7a7_0.png @@ -43,6 +57,12 @@ settings_dropdown.png=ScreenshotDropDownPreference_Light_b29dc7a7_0.png settings_slider.png=ScreenshotSliderPreference_Light_b29dc7a7_0.png settings_switch.png=ScreenshotSwitchPreference_Light_b29dc7a7_0.png settings_notifications.png=ScreenshotNotificationSection_Light_b29dc7a7_0.png +settings_appearance.png=ScreenshotAppearanceSection_Light_b29dc7a7_0.png +settings_app_info.png=ScreenshotAppInfoSection_Light_b29dc7a7_0.png +settings_persistence.png=ScreenshotPersistenceSection_Light_b29dc7a7_0.png + +# App Functions (System AI) +app-functions_settings.png=ScreenshotAppFunctionsSettings_Light_b29dc7a7_0.png # Map map_controls_overlay.png=ScreenshotMapControlsOverlay_Light_b29dc7a7_0.png @@ -52,14 +72,14 @@ settings_titled_card.png=ScreenshotTitledCard_Light_b29dc7a7_0.png settings_password_field.png=ScreenshotEditPasswordPreference_Light_b29dc7a7_0.png settings_text_field.png=ScreenshotEditTextPreference_Light_b29dc7a7_0.png settings_ipv4_field.png=ScreenshotEditIPv4Preference_Light_b29dc7a7_0.png -settings_appearance.png=ScreenshotAppearanceSection_Light_b29dc7a7_0.png - -# Messaging (conversation) -messages_message_items.png=ScreenshotMessageItem_Light_b29dc7a7_0.png -messages_reactions.png=ScreenshotReactionRow_Light_b29dc7a7_0.png -# Node detail (minimal / managed) -nodes_detail_minimal.png=ScreenshotNodeDetailContentMinimal_Light_b29dc7a7_0.png -nodes_device_metrics_card.png=ScreenshotDeviceMetricsCard_Light_b29dc7a7_0.png -nodes_environment_metrics.png=ScreenshotEnvironmentMetricsContent_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. diff --git a/screenshot-tests/docs-screenshots-manifest.txt b/screenshot-tests/docs-screenshots-manifest.txt index f97fdf6610..7bdca402cd 100644 --- a/screenshot-tests/docs-screenshots-manifest.txt +++ b/screenshot-tests/docs-screenshots-manifest.txt @@ -29,6 +29,9 @@ # Feature: Docs **/DocsScreenshotTestsKt/Screenshot*_Light_*.png +# Feature: Discovery +**/DiscoveryScreenshotTestsKt/Screenshot*_Light_*.png + # Feature: Map **/MapScreenshotTestsKt/Screenshot*_Light_*.png diff --git a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/DiscoveryScreenshotTests.kt b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/DiscoveryScreenshotTests.kt new file mode 100644 index 0000000000..2acf404518 --- /dev/null +++ b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/DiscoveryScreenshotTests.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.screenshots.feature + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.PreviewLightDark +import com.android.tools.screenshot.PreviewTest +import org.meshtastic.feature.discovery.ui.component.PreviewDiscoveryDwellProgress +import org.meshtastic.feature.discovery.ui.component.PreviewDiscoveryPresetResult + +@PreviewTest +@PreviewLightDark +@Composable +fun ScreenshotDiscoveryPresetResult() { + PreviewDiscoveryPresetResult() +} + +@PreviewTest +@PreviewLightDark +@Composable +fun ScreenshotDiscoveryDwellProgress() { + PreviewDiscoveryDwellProgress() +} diff --git a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/MessagingScreenshotTests.kt b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/MessagingScreenshotTests.kt index 278575912a..dc01a47195 100644 --- a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/MessagingScreenshotTests.kt +++ b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/MessagingScreenshotTests.kt @@ -22,6 +22,7 @@ import com.android.tools.screenshot.PreviewTest import org.meshtastic.feature.messaging.EditQuickChatDialogPreview import org.meshtastic.feature.messaging.MessageInputPreview import org.meshtastic.feature.messaging.QuickChatItemPreview +import org.meshtastic.feature.messaging.component.MessageSearchBarPreview import org.meshtastic.feature.messaging.component.ReactionItemPreview @PreviewTest @@ -51,3 +52,10 @@ fun ScreenshotMessageInput() { fun ScreenshotReactionItem() { ReactionItemPreview() } + +@PreviewTest +@PreviewLightDark +@Composable +fun ScreenshotMessageSearchBar() { + MessageSearchBarPreview() +} diff --git a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/NodeScreenshotTests.kt b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/NodeScreenshotTests.kt index f171ba17a5..e425022eac 100644 --- a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/NodeScreenshotTests.kt +++ b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/NodeScreenshotTests.kt @@ -38,6 +38,7 @@ import org.meshtastic.feature.node.detail.NodeDetailContentMinimalPreview import org.meshtastic.feature.node.detail.NodeDetailContentRemotePreview import org.meshtastic.feature.node.metrics.DeviceMetricsCardPreview import org.meshtastic.feature.node.metrics.LegendPreview +import org.meshtastic.feature.node.metrics.PreviewAirQualityCards import org.meshtastic.feature.node.metrics.PreviewEnvironmentMetricsContent @PreviewTest @@ -124,6 +125,13 @@ fun ScreenshotEnvironmentMetricsContent() { PreviewEnvironmentMetricsContent() } +@PreviewTest +@PreviewLightDark +@Composable +fun ScreenshotAirQualityCards() { + PreviewAirQualityCards() +} + // --------------------------------------------------------------------------- // Node list item screenshots (Complete + Compact densities) // --------------------------------------------------------------------------- diff --git a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/SettingsScreenshotTests.kt b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/SettingsScreenshotTests.kt index afb14f5842..951064471a 100644 --- a/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/SettingsScreenshotTests.kt +++ b/screenshot-tests/src/screenshotTest/kotlin/org/meshtastic/screenshots/feature/SettingsScreenshotTests.kt @@ -19,6 +19,7 @@ package org.meshtastic.screenshots.feature import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.PreviewLightDark import com.android.tools.screenshot.PreviewTest +import org.meshtastic.feature.settings.appfunctions.PreviewAppFunctionsSettings import org.meshtastic.feature.settings.component.AppInfoSectionPreview import org.meshtastic.feature.settings.component.AppearanceSectionPreview import org.meshtastic.feature.settings.component.NodeLayoutSettingsCompactMinimalPreview @@ -195,3 +196,10 @@ fun ScreenshotSampleNodeCompactToggleMatrix() { fun ScreenshotSampleNodeCompleteToggleMatrix() { SampleNodeCompleteToggleMatrixPreview() } + +@PreviewTest +@PreviewLightDark +@Composable +fun ScreenshotAppFunctionsSettings() { + PreviewAppFunctionsSettings() +} diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DiscoveryScreenshotTestsKt/ScreenshotDiscoveryDwellProgress_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DiscoveryScreenshotTestsKt/ScreenshotDiscoveryDwellProgress_Dark_d19fbf1f_0.png new file mode 100644 index 0000000000..be2b242913 Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DiscoveryScreenshotTestsKt/ScreenshotDiscoveryDwellProgress_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DiscoveryScreenshotTestsKt/ScreenshotDiscoveryDwellProgress_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DiscoveryScreenshotTestsKt/ScreenshotDiscoveryDwellProgress_Light_b29dc7a7_0.png new file mode 100644 index 0000000000..83dac44294 Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DiscoveryScreenshotTestsKt/ScreenshotDiscoveryDwellProgress_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DiscoveryScreenshotTestsKt/ScreenshotDiscoveryPresetResult_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DiscoveryScreenshotTestsKt/ScreenshotDiscoveryPresetResult_Dark_d19fbf1f_0.png new file mode 100644 index 0000000000..2ed9f6073c Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DiscoveryScreenshotTestsKt/ScreenshotDiscoveryPresetResult_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DiscoveryScreenshotTestsKt/ScreenshotDiscoveryPresetResult_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DiscoveryScreenshotTestsKt/ScreenshotDiscoveryPresetResult_Light_b29dc7a7_0.png new file mode 100644 index 0000000000..04e4612f47 Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/DiscoveryScreenshotTestsKt/ScreenshotDiscoveryPresetResult_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotMessageSearchBar_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotMessageSearchBar_Dark_d19fbf1f_0.png new file mode 100644 index 0000000000..b2dede9a37 Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotMessageSearchBar_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotMessageSearchBar_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotMessageSearchBar_Light_b29dc7a7_0.png new file mode 100644 index 0000000000..8c9b0ae67d Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/MessagingScreenshotTestsKt/ScreenshotMessageSearchBar_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotAirQualityCards_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotAirQualityCards_Dark_d19fbf1f_0.png new file mode 100644 index 0000000000..73646224f7 Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotAirQualityCards_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotAirQualityCards_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotAirQualityCards_Light_b29dc7a7_0.png new file mode 100644 index 0000000000..3e177a008b Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotAirQualityCards_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppFunctionsSettings_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppFunctionsSettings_Dark_d19fbf1f_0.png new file mode 100644 index 0000000000..aa7f8214b6 Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppFunctionsSettings_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppFunctionsSettings_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppFunctionsSettings_Light_b29dc7a7_0.png new file mode 100644 index 0000000000..d3cfed4378 Binary files /dev/null and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/SettingsScreenshotTestsKt/ScreenshotAppFunctionsSettings_Light_b29dc7a7_0.png differ