docs: comprehensive accuracy audit and CI fix (#5489)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
James Rich
2026-05-19 09:06:12 -05:00
committed by GitHub
parent bbdc4a3004
commit ece771edb0
28 changed files with 247 additions and 185 deletions

View File

@@ -61,7 +61,7 @@ class DocsTasks : Plugin<Project> {
bundleDir.set(outputDir.map { it.dir("common") })
schemaFile.set(
project.rootProject.layout.projectDirectory
.file("specs/003-app-docs-markdown/contracts/keyword-index-schema.json")
.file("specs/20260507-161858-app-docs-markdown/contracts/keyword-index-schema.json")
)
}

View File

@@ -28,12 +28,12 @@ color_scheme: meshtastic
# Default front-matter for pages in subdirectories
defaults:
- scope:
path: "user"
path: "user/"
values:
parent: User Guide
layout: default
- scope:
path: "developer"
path: "developer/"
values:
parent: Developer Guide
layout: default
@@ -41,229 +41,229 @@ defaults:
# They use a dedicated locale layout with a back-link to the English version.
# Auto-generated from Android app locales (values-* resource dirs).
- scope:
path: "ar"
path: "ar/"
values:
layout: locale_page
locale: ar
nav_exclude: true
- scope:
path: "be"
path: "be/"
values:
layout: locale_page
locale: be
nav_exclude: true
- scope:
path: "bg"
path: "bg/"
values:
layout: locale_page
locale: bg
nav_exclude: true
- scope:
path: "ca"
path: "ca/"
values:
layout: locale_page
locale: ca
nav_exclude: true
- scope:
path: "cs"
path: "cs/"
values:
layout: locale_page
locale: cs
nav_exclude: true
- scope:
path: "de"
path: "de/"
values:
layout: locale_page
locale: de
nav_exclude: true
- scope:
path: "el"
path: "el/"
values:
layout: locale_page
locale: el
nav_exclude: true
- scope:
path: "es"
path: "es/"
values:
layout: locale_page
locale: es
nav_exclude: true
- scope:
path: "et"
path: "et/"
values:
layout: locale_page
locale: et
nav_exclude: true
- scope:
path: "fi"
path: "fi/"
values:
layout: locale_page
locale: fi
nav_exclude: true
- scope:
path: "fr"
path: "fr/"
values:
layout: locale_page
locale: fr
nav_exclude: true
- scope:
path: "ga"
path: "ga/"
values:
layout: locale_page
locale: ga
nav_exclude: true
- scope:
path: "gl"
path: "gl/"
values:
layout: locale_page
locale: gl
nav_exclude: true
- scope:
path: "he"
path: "he/"
values:
layout: locale_page
locale: he
nav_exclude: true
- scope:
path: "hr"
path: "hr/"
values:
layout: locale_page
locale: hr
nav_exclude: true
- scope:
path: "ht"
path: "ht/"
values:
layout: locale_page
locale: ht
nav_exclude: true
- scope:
path: "hu"
path: "hu/"
values:
layout: locale_page
locale: hu
nav_exclude: true
- scope:
path: "is"
path: "is/"
values:
layout: locale_page
locale: is
nav_exclude: true
- scope:
path: "it"
path: "it/"
values:
layout: locale_page
locale: it
nav_exclude: true
- scope:
path: "ja"
path: "ja/"
values:
layout: locale_page
locale: ja
nav_exclude: true
- scope:
path: "ko"
path: "ko/"
values:
layout: locale_page
locale: ko
nav_exclude: true
- scope:
path: "lt"
path: "lt/"
values:
layout: locale_page
locale: lt
nav_exclude: true
- scope:
path: "nl"
path: "nl/"
values:
layout: locale_page
locale: nl
nav_exclude: true
- scope:
path: "no"
path: "no/"
values:
layout: locale_page
locale: no
nav_exclude: true
- scope:
path: "pl"
path: "pl/"
values:
layout: locale_page
locale: pl
nav_exclude: true
- scope:
path: "pt"
path: "pt/"
values:
layout: locale_page
locale: pt
nav_exclude: true
- scope:
path: "pt-rBR"
path: "pt-rBR/"
values:
layout: locale_page
locale: pt-rBR
nav_exclude: true
- scope:
path: "ro"
path: "ro/"
values:
layout: locale_page
locale: ro
nav_exclude: true
- scope:
path: "ru"
path: "ru/"
values:
layout: locale_page
locale: ru
nav_exclude: true
- scope:
path: "sk"
path: "sk/"
values:
layout: locale_page
locale: sk
nav_exclude: true
- scope:
path: "sl"
path: "sl/"
values:
layout: locale_page
locale: sl
nav_exclude: true
- scope:
path: "sq"
path: "sq/"
values:
layout: locale_page
locale: sq
nav_exclude: true
- scope:
path: "sr"
path: "sr/"
values:
layout: locale_page
locale: sr
nav_exclude: true
- scope:
path: "sv"
path: "sv/"
values:
layout: locale_page
locale: sv
nav_exclude: true
- scope:
path: "tr"
path: "tr/"
values:
layout: locale_page
locale: tr
nav_exclude: true
- scope:
path: "uk"
path: "uk/"
values:
layout: locale_page
locale: uk
nav_exclude: true
- scope:
path: "zh-rCN"
path: "zh-rCN/"
values:
layout: locale_page
locale: zh-rCN
nav_exclude: true
- scope:
path: "zh-rTW"
path: "zh-rTW/"
values:
layout: locale_page
locale: zh-rTW

View File

@@ -15,44 +15,56 @@
{% assign locales = site.data.locales %}
{% if locales and current_path %}
{% assign path_parts = current_path | split: "/" %}
{% assign first_segment = path_parts[0] %}
{% comment %} Build the list of available translations first {% endcomment %}
{% assign has_translations = false %}
{% if locales[first_segment] %}
{% comment %} We're on a translated page — English link is always available {% endcomment %}
{% assign has_translations = true %}
{% assign remaining_parts = path_parts | slice: 1, path_parts.size %}
{% assign en_path = remaining_parts | join: "/" | replace: ".md", "" %}
{% else %}
{% comment %} Check if any translated version exists {% endcomment %}
{% assign en_relative = current_path | replace: ".md", "" %}
{% for locale in locales %}
{% assign locale_code = locale[0] %}
{% assign locale_file = locale_code | append: "/" | append: en_relative | append: ".md" %}
{% for p in site.pages %}
{% if p.path == locale_file %}
{% assign has_translations = true %}
{% break %}
{% endif %}
{% endfor %}
{% if has_translations %}{% break %}{% endif %}
{% endfor %}
{% endif %}
{% if has_translations %}
<details class="language-switcher" aria-label="Language options">
<summary class="language-switcher-btn" title="View in another language">
🌐 <span class="lang-current">English</span>
</summary>
<ul class="language-switcher-list">
{% comment %} Always show English link back to source {% endcomment %}
{% assign path_parts = current_path | split: "/" %}
{% assign first_segment = path_parts[0] %}
{% comment %} Detect if we're currently IN a locale subdir {% endcomment %}
{% if locales[first_segment] %}
{% comment %} We're on a translated page — link back to English {% endcomment %}
{% assign remaining_parts = path_parts | slice: 1, path_parts.size %}
{% assign en_path = remaining_parts | join: "/" | replace: ".md", "" %}
<li><a href="{{ en_path | relative_url }}" lang="en">English</a></li>
{% endif %}
{% comment %} Show all available locale versions {% endcomment %}
{% for locale in locales %}
{% assign locale_code = locale[0] %}
{% assign locale_info = locale[1] %}
{% if locales[first_segment] %}
{% comment %} We're already on a translated page {% endcomment %}
{% if locale_code == first_segment %}
{% continue %}
{% endif %}
{% assign locale_path = locale_code | append: "/" | append: en_path %}
{% else %}
{% comment %} We're on an English page {% endcomment %}
{% assign en_relative = current_path | replace: ".md", "" %}
{% assign locale_path = locale_code | append: "/" | append: en_relative %}
{% endif %}
{% comment %}
Check if the translated file actually exists.
Jekyll doesn't have a file_exists filter, so we check site.pages.
{% endcomment %}
{% assign locale_file = locale_path | append: ".md" %}
{% assign page_exists = false %}
{% for p in site.pages %}
@@ -69,3 +81,4 @@
</ul>
</details>
{% endif %}
{% endif %}

View File

@@ -30,7 +30,6 @@ Then copy the relevant light-mode PNGs from the reference directory. The
Examples:
- `onboarding_welcome.png`
- `connections_bluetooth_scan.png`
- `messages-and-channels_channel_list.png`
- `firmware_disclaimer.png`
## Guidelines

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -3,7 +3,6 @@ title: Developer Guide
layout: default
nav_order: 2
has_children: true
parent: ""
---
# Developer Guide

View File

@@ -23,15 +23,10 @@ mkdir -p feature/my-feature/src/{commonMain,commonTest,androidMain,jvmMain,iosMa
```kotlin
plugins {
alias(libs.plugins.meshtastic.kmp.feature)
alias(libs.plugins.meshtastic.kotlinx.serialization)
id("meshtastic.kmp.jvm.android")
}
kotlin {
android {
namespace = "org.meshtastic.feature.myfeature"
androidResources.enable = false
}
androidLibrary { withHostTest { } }
sourceSets {
commonMain.dependencies {
@@ -82,8 +77,8 @@ class FeatureMyFeatureModule
## 5. Register DI in App/Desktop
Add your module to:
- `app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt`
- `desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt`
- `androidApp/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt`
- `desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt`
## 6. Add Navigation Routes

View File

@@ -16,7 +16,7 @@ The Meshtastic Android/Desktop/iOS application follows a modular Kotlin Multipla
```
┌─────────────────────────────────────────────┐
app / desktop │ Platform entry points
androidApp / desktopApp │ Platform entry points
├─────────────────────────────────────────────┤
│ feature/* modules │ UI + Business Logic
├─────────────────────────────────────────────┤
@@ -28,7 +28,7 @@ The Meshtastic Android/Desktop/iOS application follows a modular Kotlin Multipla
## Module Categories
### `app/` — Android Application
### `androidApp/` — Android Application
The Android application entry point:
- Activity, Application, and Manifest definitions
@@ -36,13 +36,13 @@ The Android application entry point:
- Flavor-specific bindings (`google/`, `fdroid/`)
- Android-only integrations (widgets, services)
### `desktop/` — Desktop JVM Application
### `desktopApp/` — Desktop JVM Application
The Desktop (Linux/macOS/Windows) entry point:
- Compose Desktop window management
- Desktop-specific DI (`DesktopKoinModule`)
- Platform stubs for Android-only capabilities
- Serial transport implementation
- BLE (Kable), Serial, and TCP transport implementations
### `feature/*` — Feature Modules

View File

@@ -16,11 +16,11 @@ Repository layout, namespacing conventions, and build system overview.
```
Meshtastic-Android/
├── app/ # Android application module
├── androidApp/ # Android application module
│ ├── src/main/ # Shared Android code
│ ├── src/google/ # Google Play flavor (Gemini, proprietary)
│ └── src/fdroid/ # F-Droid flavor (FOSS-only)
├── desktop/ # Desktop JVM application
├── desktopApp/ # Desktop JVM application
├── feature/ # Feature modules (KMP)
│ ├── intro/
│ ├── messaging/
@@ -33,22 +33,27 @@ Meshtastic-Android/
│ ├── wifi-provision/
│ └── widget/
├── core/ # Core infrastructure modules (KMP)
│ ├── api/
│ ├── barcode/
│ ├── ble/
│ ├── common/
│ ├── navigation/
│ ├── ui/
│ ├── resources/
│ ├── model/
│ ├── data/
│ ├── database/
│ ├── datastore/
│ ├── prefs/
│ ├── repository/
│ ├── service/
│ ├── di/
│ ├── domain/
│ ├── model/
│ ├── navigation/
│ ├── network/
│ ├── ble/
│ ├── nfc/
│ ├── prefs/
│ ├── proto/
── testing/
── repository/
│ ├── resources/
│ ├── service/
│ ├── takserver/
│ ├── testing/
│ └── ui/
├── build-logic/ # Convention plugins and build helpers
│ └── convention/
├── docs/ # Documentation source (markdown)
@@ -115,7 +120,7 @@ Located in `build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/`:
./gradlew assembleGoogleDebug assembleFdroidDebug
# Desktop run
./gradlew :desktop:run
./gradlew :desktopApp:run
```
## Version Catalog Highlights

View File

@@ -14,15 +14,26 @@ Guidelines for contributing to the Meshtastic Android/Desktop/iOS project.
## Branch Naming
Feature branches follow the pattern:
```
{issue-number}-{short-description}
```
Branches use conventional-commit style prefixes:
| Prefix | Use for |
|--------|---------|
| `feat/<scope>` | New user-visible behavior |
| `fix/<scope>` | Bug fixes |
| `refactor/<scope>` | Code structure changes |
| `chore/<scope>` | Tooling, deps, CI, cleanup |
| `docs/<scope>` | Documentation only |
| `build/<scope>` | Build system changes |
| `ci/<scope>` | CI workflow changes |
| `test/<scope>` | Test additions or fixes |
| `deps/<scope>` | Dependency updates |
Numeric spec prefixes (e.g., `003-app-docs-markdown`) are also valid for spec-driven work.
Examples:
- `003-app-docs-markdown`
- `001-local-mesh-discovery`
- `feat/desktop-ble-transport`
- `fix/bluetooth-reconnect`
- `003-app-docs-markdown`
## Development Workflow

View File

@@ -24,7 +24,7 @@ interface Graph : Route // Graph roots for navigation hierarchies
@Serializable
sealed interface SettingsRoute : Route {
@Serializable data class SettingsGraph(val destNum: Int?) : SettingsRoute, Graph
@Serializable data class Settings(val destNum: Int? = null) : SettingsRoute, Graph
@Serializable data object DeviceConfiguration : SettingsRoute
@Serializable data object HelpDocs : SettingsRoute
@Serializable data class HelpDocPage(val pageId: String) : SettingsRoute
@@ -54,7 +54,7 @@ meshtastic://meshtastic/{path}
| URI Path | Route | Notes |
|----------|-------|-------|
| `/settings` | `SettingsRoute.SettingsGraph(null)` | Settings root |
| `/settings` | `SettingsRoute.Settings(null)` | Settings root |
| `/settings/helpDocs` | `SettingsRoute.HelpDocs` | Docs browser |
| `/settings/helpDocs/{pageId}` | `SettingsRoute.HelpDocPage(pageId)` | Specific doc page |
| `/settings/help-docs` | `SettingsRoute.HelpDocs` | Compatibility alias |
@@ -70,7 +70,7 @@ Deep links synthesize a full backstack, not just the target screen:
```kotlin
// /settings/helpDocs/messages-and-channels produces:
listOf(
SettingsRoute.SettingsGraph(null),
SettingsRoute.Settings(null),
SettingsRoute.HelpDocs,
SettingsRoute.HelpDocPage("messages-and-channels"),
)
@@ -90,9 +90,9 @@ This ensures the user can navigate "up" correctly.
Each feature module provides entries via an extension function:
```kotlin
fun EntryProviderScope<*>.docsEntries(backStack: NavBackStack) {
fun EntryProviderScope<NavKey>.docsEntries(backStack: NavBackStack<NavKey>) {
entry<SettingsRoute.HelpDocs> { DocsBrowserScreen(backStack) }
entry<SettingsRoute.HelpDocPage> { DocsPageRouteScreen(it.pageId, backStack) }
entry<SettingsRoute.HelpDocPage> { route -> DocsPageRouteScreen(route.pageId, backStack) }
}
```
@@ -105,17 +105,5 @@ Deep link routing is tested in:
core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/DeepLinkRouterTest.kt
```
Example:
```kotlin
@Test
fun `help docs deep link routes correctly`() {
val result = DeepLinkRouter.route(CommonUri.parse("meshtastic://meshtastic/settings/helpDocs"))
assertEquals(
listOf(SettingsRoute.SettingsGraph(null), SettingsRoute.HelpDocs),
result,
)
}
```
---

View File

@@ -35,20 +35,30 @@ The primary structured data store:
| Entity | Description |
|--------|-------------|
| Nodes | All known mesh nodes and metadata |
| Messages | Message history (channel and direct) |
| Waypoints | Shared geographic points |
| Telemetry | Device, environment, power metrics |
| Channels | Channel configurations |
| `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 |
| `ContactSettings` | Per-contact mute and read-state |
| `ReactionEntity` | Emoji reactions on messages |
| `MeshLog` | Raw mesh protocol logs |
| `MetadataEntity` | Device metadata (firmware version, hardware model) |
| `QuickChatAction` | User-configured quick-chat messages |
| `DeviceHardwareEntity` | Cached device hardware catalog |
| `FirmwareReleaseEntity` | Cached firmware release info |
| `TracerouteNodePositionEntity` | Traceroute hop position data |
> 💡 **Note:** Waypoints, telemetry, and channel data are stored within the `Packet` entity (using the `port_num` field to distinguish packet types) rather than in separate tables.
## DataStore Preferences
**Module:** `core:datastore`
For lightweight key-value preferences:
- Connection state
- Last connected device
- UI preferences
- Local radio configuration (LocalConfig proto)
- Module configuration (ModuleConfig proto)
- Channel set data
- Local statistics
- Recently connected device addresses
## Core Prefs

View File

@@ -55,12 +55,12 @@ Located in `commonTest` or `jvmTest` source sets.
### Screenshot Tests
Preferred: **Roborazzi** (Gradle-native, Ubuntu CI compatible)
Fallback: **Paparazzi** (Android-view-centric)
Uses Android Gradle Plugin's native screenshot testing framework:
```bash
./gradlew recordDocsScreenshots # Record golden images
./gradlew verifyDocsScreenshots # Compare against goldens
./gradlew :screenshot-tests:updateDebugScreenshotTest # Record golden images
./gradlew :screenshot-tests:validateDebugScreenshotTest # Compare against goldens
./gradlew :screenshot-tests:copyDocsScreenshots # Copy reference images to docs pipeline
```
## Test Organization

View File

@@ -24,9 +24,9 @@ App ← RadioController → Transport (BLE | Serial | TCP)
## Bluetooth Low Energy (BLE)
**Module:** `core:ble`
**Platforms:** Android, (planned: iOS)
**Platforms:** Android, Desktop (JVM via Kable), iOS (planned)
The primary transport for Android mobile devices:
The primary transport for mobile devices and also available on desktop:
- Service discovery for Meshtastic GATT services
- Characteristic-based read/write for protobuf packets
- Connection state management and automatic reconnection
@@ -35,7 +35,7 @@ The primary transport for Android mobile devices:
### Key Classes
- `core/ble/` — BLE scanning, connection, and GATT operations
- Platform-specific implementations in `androidMain`
- Platform-specific implementations in `androidMain` and `jvmMain` (Kable)
## USB Serial
@@ -51,7 +51,7 @@ Serial communication over USB:
### Key Classes
- Serial prober and transport factory in `core/network`
- Desktop-specific serial in `desktop/src/main/kotlin/.../radio/`
- Desktop-specific serial in `desktopApp/src/main/kotlin/.../radio/`
## TCP/IP
@@ -70,13 +70,17 @@ The `RadioTransportFactory` interface abstracts transport creation:
```kotlin
interface RadioTransportFactory {
fun createTransport(config: TransportConfig): RadioTransport
val supportedDeviceTypes: List<DeviceType>
fun createTransport(address: String, service: RadioInterfaceService): RadioTransport
fun isMockTransport(): Boolean
fun isAddressValid(address: String?): Boolean
fun toInterfaceAddress(interfaceId: InterfaceId, rest: String): String
}
```
Platform-specific implementations:
- **Android:** Supports BLE + USB + TCP
- **Desktop:** Supports USB + TCP (no BLE)
- **Desktop:** Supports BLE (Kable) + USB + TCP
- **iOS:** Planned BLE + TCP
## Connection Lifecycle

View File

@@ -10,14 +10,17 @@ This documentation is translated by the community via [Crowdin](https://crowdin.
## Available Languages
{% assign any_locale_exists = false %}
{% for locale in site.data.locales %}
{% assign locale_code = locale[0] %}
{% assign locale_info = locale[1] %}
{% assign locale_index = locale_code | append: "/index.md" %}
{% assign locale_prefix = locale_code | append: "/" %}
{% assign has_content = false %}
{% for p in site.pages %}
{% if p.path contains locale_code %}
{% assign page_path_check = p.path | slice: 0, locale_prefix.size %}
{% if page_path_check == locale_prefix %}
{% assign has_content = true %}
{% assign any_locale_exists = true %}
{% break %}
{% endif %}
{% endfor %}
@@ -27,19 +30,6 @@ This documentation is translated by the community via [Crowdin](https://crowdin.
{% endif %}
{% endfor %}
{% comment %} Show notice if no translations exist yet {% endcomment %}
{% assign any_locale_exists = false %}
{% for locale in site.data.locales %}
{% assign locale_code = locale[0] %}
{% for p in site.pages %}
{% if p.path contains locale_code %}
{% assign any_locale_exists = true %}
{% break %}
{% endif %}
{% endfor %}
{% if any_locale_exists %}{% break %}{% endif %}
{% endfor %}
{% unless any_locale_exists %}
> No translations available yet. Want to help? [Join our Crowdin project →](https://crowdin.com/project/meshtastic-android)
{% endunless %}

View File

@@ -3,7 +3,6 @@ title: User Guide
layout: default
nav_order: 1
has_children: true
parent: ""
---
# User Guide

View File

@@ -80,25 +80,36 @@ Some Meshtastic radios support WiFi connectivity, allowing TCP-based connections
3. Enter the radio's IP address and port (default: 4403).
4. Tap **Connect**.
![WiFi scanning for devices](/assets/screenshots/connections_wifi_scanning.png)
When a device is found, it appears in the connection list:
![WiFi device found](/assets/screenshots/connections_wifi_device_found.png)
A successful connection is confirmed with a status indicator:
![WiFi connection success](/assets/screenshots/connections_wifi_success.png)
### When to Use TCP
- Radio is on the same local network
- Testing with a simulated radio
- Environments where Bluetooth has interference issues
## Connection Priority
## Reconnection Behavior
The app attempts connections in this order:
1. Last successful Bluetooth device
2. USB (if detected)
3. Manual TCP (if configured)
The app reconnects to the **last selected device** on startup. You can manually switch transports from the connections screen at any time.
To disconnect from a radio, use the disconnect button on the connections screen:
![Disconnect from radio](/assets/screenshots/connections_disconnect.png)
## Desktop Connections
On Desktop (Linux/macOS/Windows), the app supports:
- **USB Serial** — primary connection method
- **Bluetooth (BLE)** — via the Kable library; works on macOS, Linux, and Windows
- **USB Serial** — primary wired connection method
- **TCP/IP** — for network-connected radios
- Bluetooth is **not** currently supported on Desktop
See [Desktop App](desktop) for platform-specific details and keyboard shortcuts.

View File

@@ -24,7 +24,7 @@ The Desktop app shares its core codebase with the Android app through Kotlin Mul
### Linux
- Download the `.deb` or `.AppImage` package from the releases page
- Or build from source using `./gradlew :desktop:run`
- Or build from source using `./gradlew :desktopApp:run`
### macOS
@@ -53,9 +53,13 @@ For network-connected radios:
1. Enter the radio's IP address and port (default: 4403).
2. Click **Connect**.
### Bluetooth
### Bluetooth (BLE)
> ⚠️ **Note:** Bluetooth is not currently supported on the Desktop app. Use USB or TCP connections.
Bluetooth Low Energy is supported on Desktop via the [Kable](https://github.com/JuulLabs/kable) library:
1. Ensure your system has a Bluetooth adapter.
2. The app scans for nearby Meshtastic radios automatically.
3. Select your device from the connections screen.
## Feature Parity
@@ -65,7 +69,7 @@ For network-connected radios:
| Node List | ✓ | ✓ | Full parity |
| Map | ✓ | ✓ | Full parity |
| Settings | ✓ | ✓ | Full parity |
| Bluetooth | ✓ | | USB/TCP on desktop |
| Bluetooth (BLE) | ✓ | | Via Kable on desktop |
| Firmware Update OTA | ✓ | ✗ | Use web flasher |
| Notifications | ✓ | ✓ | Native OS notifications |
| Widgets | ✓ | ✗ | Android-only |
@@ -99,13 +103,27 @@ The Desktop app uses the same Compose Multiplatform UI with adaptations for larg
The Desktop app provides in-app toggles for controlling which notifications are shown — messages, new nodes, and low battery alerts. Access these from **Settings → Notifications** within the app.
## Built-in Documentation Browser
The Desktop app includes a built-in documentation browser for quick access to help content without leaving the application.
![Docs browser with table of contents](/assets/screenshots/docs-browser_toc.png)
The browser supports full-text search across all documentation:
![Searching the docs browser](/assets/screenshots/docs-browser_search.png)
Individual doc pages render with full formatting:
![A documentation page](/assets/screenshots/docs-browser_page.png)
## Building from Source
```bash
git clone https://github.com/meshtastic/Meshtastic-Android.git
cd Meshtastic-Android
git submodule update --init
./gradlew :desktop:run
./gradlew :desktopApp:run
```
Requirements:
@@ -114,10 +132,10 @@ Requirements:
## Known Limitations
- No Bluetooth support
- No OTA firmware updates (use web flasher)
- Some Android-specific features (widgets, specific notification channels) are unavailable
- Performance may vary on low-spec hardware running Compose Desktop
- BLE bonding is not yet supported on desktop (pairing works without bonding)
## Related Topics

View File

@@ -96,7 +96,7 @@ The node list itself is a powerful discovery tool when you use its filtering and
### Infrastructure Audit
- Disable **Exclude infrastructure** to see Router, Repeater, and Router Client nodes.
- Disable **Exclude infrastructure** to see Router, Repeater, Router Late, and Client Base nodes.
- Check their signal quality and last-heard times to verify your infrastructure nodes are healthy.
See [Nodes](nodes) for full details on filtering and sorting options.

View File

@@ -78,6 +78,8 @@ If the update appears frozen:
- If truly stuck, power-cycle the radio
- Attempt the update again
![Firmware update error](/assets/screenshots/firmware_error.png)
### Device Won't Boot After Update
If your device fails to boot:

View File

@@ -42,6 +42,8 @@ Channels support multiple encryption levels:
3. Configure the channel name and encryption key.
4. Share the channel URL/QR code with others who need access.
Tapping a channel shows its details and sharing options.
## Direct Messages
Direct messages (DMs) are point-to-point encrypted communications between two specific nodes.
@@ -57,8 +59,11 @@ Direct messages (DMs) are point-to-point encrypted communications between two sp
| State | Icon | Meaning |
|-------|------|---------|
| Queued | ⏳ | Message waiting to be sent |
| Sent | ✓ | Message transmitted to mesh |
| En route | ✓ | Delivered to the radio, awaiting acknowledgment |
| Delivered | ✓✓ | Acknowledgment received from recipient |
| Received | ✓ | Message received from the mesh (incoming) |
| S&F Routing | 🔗 | Store & Forward: message being routed through an S&F node |
| S&F Confirmed | 🔗 | Store & Forward: delivery confirmed via S&F node |
| Error | ✗ | Delivery failed after retries |
### Delivery Errors
@@ -92,9 +97,7 @@ Pre-configured messages for rapid communication:
![Quick chat option](/assets/screenshots/messages_quick_chat.png)
The channel list shows each channel with its latest message preview:
![Channel list item showing channel name and last message](/assets/screenshots/messages-and-channels_channel_list.png)
The channel list shows each channel with its latest message preview.
### Message Bubbles

View File

@@ -2,7 +2,7 @@
title: MQTT
nav_order: 11
last_updated: 2026-05-13
description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, JSON output, and map reporting.
description: Bridge your mesh to the internet — MQTT broker setup, encryption layers, and map reporting.
aliases:
- mqtt
- internet-bridge
@@ -46,7 +46,7 @@ A gateway node with internet access (WiFi or Ethernet) publishes mesh messages t
| Password | Broker authentication | large4cats |
| Root Topic | Base topic for messages | msh |
| Encryption | Encrypt MQTT payload | Enabled |
| JSON Output | Publish JSON alongside protobuf | Disabled |
| ~~JSON Output~~ | ⚠️ **Deprecated** — JSON packet support has been removed from firmware; this field is ignored | Disabled |
| TLS | Secure connection to broker | Disabled |
| Map Reporting | Report position to public map | Disabled |
@@ -83,14 +83,13 @@ Configure per-channel which directions are active to control message flow and ai
## Message Formats
MQTT supports two message formats:
MQTT uses protobuf message format:
| Format | Description | Use case |
|--------|-------------|----------|
| **Protobuf** (default) | Binary Meshtastic protobuf encoding | Node-to-node mesh bridging |
| **JSON** | Human-readable JSON encoding | Home automation, logging, custom integrations |
| **Protobuf** | Binary Meshtastic protobuf encoding | Node-to-node mesh bridging |
When **JSON Output** is enabled, the gateway publishes both protobuf and JSON versions of each message to separate topics.
> ⚠️ **Note:** JSON output support was removed from firmware. The `json_enabled` setting is still visible in the app for legacy compatibility but has no effect on current firmware versions.
## Encryption & Privacy

View File

@@ -39,11 +39,13 @@ Nodes can be configured with different roles that affect their mesh behavior:
| Role | Description |
|------|-------------|
| Client | Standard end-user device |
| Client Base | Treats favorited-node traffic as Router Late priority; all other traffic as Client |
| Client Mute | Receives but doesn't retransmit |
| Client Hidden | Like Client Mute, plus hides from node list |
| Router | Prioritizes message forwarding; stays awake to relay |
| Router Client | Routes and operates as a client |
| Repeater | Retransmits only; no user interface |
| Router Late | Infrastructure node that rebroadcasts once, but only after all other modes (provides supplemental coverage) |
| ~~Router Client~~ | ⚠️ **Deprecated** (removed in firmware 2.3.15) — no longer selectable; use Router or Client instead |
| ~~Repeater~~ | ⚠️ **Deprecated** (removed in firmware 2.7.11) — no longer selectable; use Router instead |
| Tracker | Optimized for position reporting at regular intervals |
| Sensor | Optimized for telemetry reporting |
| TAK | Interoperates with TAK systems (sends/receives CoT) |
@@ -55,9 +57,9 @@ Nodes can be configured with different roles that affect their mesh behavior:
Most users should keep the default **Client** role. Consider a different role when:
- **Router** — You have a node in a fixed, elevated location with reliable power (rooftop, hilltop). Routers stay awake continuously to relay messages for others and are essential for extending mesh coverage. Don't use Router on battery-powered handheld devices.
- **Router Client** — Like Router, but the device is also used as a personal client. Good for a home base station that you also send messages from.
- **Router Late** — An infrastructure node that always rebroadcasts packets once but only after all other routing modes have had their turn. Provides supplemental coverage for local clusters without competing with primary routers.
- **Client Base** — Treats traffic from/to your favorited nodes with Router Late priority (ensuring those messages get extra relay coverage) while handling everything else as a normal Client.
- **Client Mute** — You want to receive mesh traffic but not contribute to relaying. Useful for monitoring-only devices or to reduce congestion in dense areas.
- **Repeater** — A dedicated relay node with no screen or user interaction. Optimized purely for forwarding; lowest power consumption of the relay roles.
- **Tracker** — An unattended device whose sole purpose is broadcasting its GPS position (e.g., a vehicle, pet, or asset). Sleeps between broadcasts to conserve battery.
- **Sensor** — An unattended device reporting environmental telemetry (temperature, humidity, air quality). Similar power profile to Tracker.
- **TAK / TAK Tracker** — Only needed if interoperating with ATAK/WinTAK systems. See [TAK Integration](tak) for details.
@@ -100,7 +102,7 @@ Type in the search field to filter nodes by name or short name. The filter updat
| **Only online** | Show only nodes heard within the last 15 minutes |
| **Only direct** | Show only nodes with direct (non-relayed) connections |
| **Include unknown** | Show nodes that haven't sent user info yet |
| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Repeater, Router Client) |
| **Exclude infrastructure** | Hide infrastructure-role nodes (Router, Repeater, Router Late, Client Base) |
| **Exclude MQTT** | Hide nodes heard only via MQTT internet bridge |
| **Show ignored** | Show nodes you've previously dismissed or muted |

View File

@@ -21,6 +21,10 @@ Module settings use a card-based layout with toggle switches, dropdowns, text fi
![Dropdown selector](/assets/screenshots/settings_dropdown.png)
![Text field](/assets/screenshots/settings_text_field.png)
![Settings card layout](/assets/screenshots/settings_titled_card.png)
## Module Configuration
### MQTT Module
@@ -34,7 +38,7 @@ Bridges mesh messages to and from an MQTT broker for internet connectivity. This
| Username | Authentication username |
| Password | Authentication password |
| Encryption | Encrypt MQTT payloads |
| JSON Output | Also publish in JSON format |
| ~~JSON Output~~ | ⚠️ **Deprecated** — JSON support removed from firmware; field is ignored |
| TLS | Use secure connection |
| Root Topic | Base MQTT topic path |
| Map Report | Publish position for public map |
@@ -112,7 +116,7 @@ Pre-configured messages accessible from the device's physical buttons (for radio
| Setting | Description |
|---------|-------------|
| Enabled | Activate canned messages |
| ~~Enabled~~ | ⚠️ **Deprecated** — current firmware may ignore this toggle |
| Messages | Newline-separated list of messages |
| Send Bell | Play bell sound on send |
| Rotary Encoder | Enable rotary encoder input |

View File

@@ -28,8 +28,6 @@ Configure your radio hardware and user identity parameters.
After modifying settings, tap **Save** to write the configuration to your radio. The device may reboot to apply changes.
![Settings appearance section](/assets/screenshots/settings-radio-user_lora_config.png)
## Radio Configuration
### Device Config
@@ -37,8 +35,6 @@ After modifying settings, tap **Save** to write the configuration to your radio.
| Setting | Description | Default |
|---------|-------------|---------|
| Role | Node behavior (Client, Router, etc.) | Client |
| Serial Output | Enable serial console output | Disabled |
| Debug Log | Enable verbose debug logging | Disabled |
| Rebroadcast Mode | How the node retransmits messages | All |
| Node Info Broadcast (s) | Interval for broadcasting node info | 10800 |
| Double-tap Button | Action for double-tap button press | Disabled |
@@ -62,11 +58,18 @@ After modifying settings, tap **Save** to write the configuration to your radio.
|--------|-------|-------|-----------|----------|
| Short Turbo | ~1 km | 21.9 kbps | 5 dB | Dense urban with line-of-sight; data-heavy applications |
| Short Fast | ~3 km | 10.9 kbps | 7.5 dB | Urban neighborhoods; buildings within a few blocks |
| Short Slow | ~5 km | 5.5 kbps | 10 dB | Suburban short-range; moderate building density |
| Medium Fast | ~5 km | 5.5 kbps | 10 dB | Suburban areas; moderate building density |
| Medium Slow | ~8 km | 1.1 kbps | 12.5 dB | Suburban/rural; moderate range with slower speed |
| Long Turbo | ~10 km | 4.4 kbps | 10 dB | Similar range to Long Fast but with 500 kHz bandwidth; faster throughput |
| Long Fast | ~10 km | 1.1 kbps | 12.5 dB | **General use (default)** — balanced range and speed |
| Long Moderate | ~20 km | 0.34 kbps | 15 dB | Rural with some terrain; occasional use |
| Long Slow | ~30 km | 0.18 kbps | 17.5 dB | Sparse rural; maximum reliable range |
| Very Long Slow | ~40+ km | 0.09 kbps | 20 dB | Extreme range experiments; very slow throughput |
| Lite Fast | ~5 km | 5.5 kbps | 10 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Medium Fast |
| Lite Slow | ~10 km | 1.1 kbps | 12.5 dB | EU 866 MHz SRD band (125 kHz BW); comparable to Long Fast |
| Narrow Fast | ~5 km | 2.7 kbps | 10 dB | EU 868 MHz band (62.5 kHz BW); avoids interference with other devices |
| Narrow Slow | ~10 km | 1.1 kbps | 12.5 dB | EU 868 MHz band (62.5 kHz BW); comparable to Long Fast |
| ~~Long Slow~~ | ~30 km | 0.18 kbps | 17.5 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release |
| ~~Very Long Slow~~ | ~40+ km | 0.09 kbps | 20 dB | ⚠️ **Deprecated** — still selectable but may be removed in a future firmware release |
#### Choosing a Modem Preset
@@ -78,8 +81,9 @@ The modem preset controls the fundamental tradeoff between **range** and **data
**Practical guidance:**
- **Urban mesh (many nodes, short distances):** Use **Long Fast** (default) or **Short Fast**. Higher speed means less airtime congestion when many nodes share the channel.
- **Rural/sparse mesh (few nodes, long distances):** Use **Long Moderate** or **Long Slow**. Range matters more than speed when nodes are far apart.
- **Fixed infrastructure links:** Use **Short Turbo** for dedicated point-to-point links with good antennas and line-of-sight.
- **Rural/sparse mesh (few nodes, long distances):** Use **Long Moderate**. Range matters more than speed when nodes are far apart.
- **EU 866/868 MHz regulatory compliance:** Use **Lite Fast**, **Lite Slow**, **Narrow Fast**, or **Narrow Slow** — these are optimized for the EU SRD/868 MHz bands with narrower bandwidths.
- **Fixed infrastructure links:** Use **Short Turbo** or **Long Turbo** for dedicated point-to-point links with good antennas and line-of-sight.
- **Mixed environments:** Stick with **Long Fast** — it's the community default and ensures compatibility with others in your area.
> ⚠️ **Important:** All nodes on the same channel **must** use the same modem preset. Nodes with mismatched presets cannot communicate even if they share the same frequency and encryption key.
@@ -92,9 +96,9 @@ The modem preset controls the fundamental tradeoff between **range** and **data
|---------|-------------|
| Screen Timeout | Time before display sleeps |
| Display Units | Metric or Imperial |
| GPS Format | DMS, Decimal, UTM, MGRS, OLC |
| OLED Type | Auto, SSD1306, SH1106, SH1107 |
| Compass North | True North or Magnetic North |
| Compass Orientation | Rotation offset for compass display (0°, 90°, 180°, 270°) |
| ~~Compass North~~ | ⚠️ **Deprecated** — replaced by Compass Orientation; still visible in older firmware |
### Position Config
@@ -126,6 +130,8 @@ The modem preset controls the fundamental tradeoff between **range** and **data
| NTP Server | Time synchronization server |
| Syslog Server | Remote logging server |
![IP address field](/assets/screenshots/settings_ipv4_field.png)
### Bluetooth Config
| Setting | Description |
@@ -142,8 +148,12 @@ The modem preset controls the fundamental tradeoff between **range** and **data
| Admin Key | Key for remote administration |
| Private Key | Your node's private key (handle securely) |
| Admin Channel Enabled | Allow admin commands via channel |
| Debug Log | Output live debug logging over serial/bluetooth |
| Serial Enabled | Enable serial console access (moved from Device Config) |
| Managed Mode | Restrict non-admin channel changes |
![Password field](/assets/screenshots/settings_password_field.png)
Settings use standard preference controls — dropdowns, toggles, and sliders:
| Control | Screenshot |

View File

@@ -52,10 +52,10 @@ Here is exactly how the app decides how many bars (or what color) to show you:
| Level | Bars | Criteria | Meaning |
|-------|------|----------|---------|
| Good | 3 | RSSI better than `-115 dBm` **AND** SNR above the baseline limit for your preset | Signal is both loud and clear — healthy connection. |
| Fair | 2 | Falls between Good and Bad | Signal getting quieter or noisier, but the radio understands the message fine. |
| Bad | 1 | RSSI drops to `-120 dBm` or worse, **OR** SNR within `5.5 dB` of your preset's absolute breaking point | Barely hanging on — at the edge of range or heavy interference. |
| None | 0 | RSSI worse than `-126 dBm` **AND** SNR has fallen `7.5 dB` below the ideal limit | Transmission completely buried in static. |
| Good | 3 | RSSI better than `-115 dBm` **AND** SNR better than `-7 dB` | Signal is both loud and clear — healthy connection. |
| Fair | 2 | RSSI better than `-126 dBm` with good SNR, **OR** SNR better than `-15 dB` with good RSSI | Signal getting quieter or noisier, but still decodable. |
| Bad | 1 | Falls between Fair and None thresholds | At the edge of range or experiencing interference. |
| None | 0 | RSSI worse than `-126 dBm` **AND** SNR worse than `-15 dB` | Transmission completely buried in noise. |
---