docs: publish channels: main api

This commit is contained in:
github-actions[bot] committed 2026-07-24 19:31:43 +00:00
commit 8ebb84c5ef
10921 files changed
+1871564

No files matched your search

@@ -0,0 +1,146 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Adding a Feature Module</title>
<link rel="stylesheet" href="../styles/docs.css">
</head>
<body data-page="adding-a-feature-module" data-locale="en">
<pre class="markdown-content"># Adding a Feature Module
Step-by-step guide for creating a new KMP feature module in the Meshtastic project.
## 1. Create the Module Directory
```bash
mkdir -p feature/my-feature/src/{commonMain,commonTest,androidMain,jvmMain,iosMain}/kotlin/org/meshtastic/feature/myfeature
```
## 2. Create `build.gradle.kts`
```kotlin
plugins {
alias(libs.plugins.meshtastic.kmp.feature)
}
kotlin {
android { withHostTest { isIncludeAndroidResources = true } }
sourceSets {
commonMain.dependencies {
implementation(projects.core.common)
implementation(projects.core.navigation)
implementation(projects.core.resources)
implementation(projects.core.ui)
implementation(projects.core.di)
}
commonTest.dependencies {
implementation(libs.compose.multiplatform.ui.test)
}
jvmTest.dependencies {
implementation(compose.desktop.currentOs)
}
}
}
```
## 3. Register in `settings.gradle.kts`
Add your module to the main `include()` block:
```kotlin
include(
// ...existing modules...
":feature:my-feature",
)
```
## 4. Create the DI Module
`src/commonMain/kotlin/org/meshtastic/feature/myfeature/di/FeatureMyFeatureModule.kt`:
```kotlin
package org.meshtastic.feature.myfeature.di
import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Module
@Module
@ComponentScan("org.meshtastic.feature.myfeature")
class FeatureMyFeatureModule
```
## 5. Register DI in App/Desktop
Add your module to:
- `androidApp/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt`
- `desktopApp/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt`
## 6. Add Navigation Routes
In `core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt`:
```kotlin
@Serializable
sealed interface MyFeatureRoute : Route {
@Serializable data object MyFeatureGraph : MyFeatureRoute, Graph
@Serializable data object MyFeatureHome : MyFeatureRoute
}
```
## 7. Create Navigation Entries
`src/commonMain/kotlin/org/meshtastic/feature/myfeature/navigation/MyFeatureNavigation.kt`:
```kotlin
package org.meshtastic.feature.myfeature.navigation
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavBackStack
import androidx.navigation3.runtime.NavKey
import org.meshtastic.core.navigation.MyFeatureRoute
fun EntryProviderScope&lt;NavKey&gt;.myFeatureGraph(backStack: NavBackStack&lt;NavKey&gt;) {
entry&lt;MyFeatureRoute.MyFeatureHome&gt; {
MyFeatureScreen()
}
}
```
## 8. Source Set Guidelines
Source Set | Contains |
-----------|----------|
`commonMain` | Models, ViewModels, shared UI, DI module, navigation |
`androidMain` | Android-specific implementations (e.g., platform APIs) |
`jvmMain` | Desktop-specific implementations |
`iosMain` | iOS-specific implementations |
`commonTest` | Shared unit tests |
## 9. Testing Expectations
Every feature module should have:
- Unit tests in `commonTest` for business logic
- UI tests using `compose-multiplatform-ui-test` where appropriate
- No test dependency on other feature modules
## 10. Checklist
- [ ] Module directory created
- [ ] `build.gradle.kts` with correct plugins and dependencies
- [ ] Added to `settings.gradle.kts`
- [ ] DI module created with `@ComponentScan`
- [ ] DI module registered in app and desktop roots
- [ ] Routes added to `Routes.kt`
- [ ] Navigation entries registered
- [ ] `./gradlew kmpSmokeCompile` passes
- [ ] `./gradlew :feature:my-feature:allTests` passes
---
</pre>
</body>
</html>
+180
View File
@@ -0,0 +1,180 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Architecture</title>
<link rel="stylesheet" href="../styles/docs.css">
</head>
<body data-page="architecture" data-locale="en">
<pre class="markdown-content"># Architecture
The Meshtastic Android and Desktop apps follow a modular Kotlin Multiplatform (KMP) architecture with clear layer boundaries (iOS is currently a compile-only validation target — there is no shipping iOS app yet).
## Layer Overview
```
┌─────────────────────────────────────────────┐
│ androidApp / desktopApp │ Platform entry points
├─────────────────────────────────────────────┤
│ feature/* modules │ UI + Business Logic
├─────────────────────────────────────────────┤
│ core/* modules │ Shared infrastructure
├─────────────────────────────────────────────┤
│ Platform (Android/JVM/iOS) │ OS-specific bindings
└─────────────────────────────────────────────┘
```
## Module Categories
### `androidApp/` — Android Application
The Android application entry point:
- Activity, Application, and Manifest definitions
- Koin DI module composition (`AppKoinModule`)
- Flavor-specific bindings (`google/`, `fdroid/`)
- Android-only integrations (widgets, services)
### `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
- BLE (Kable), Serial, and TCP transport implementations
### `feature/*` — Feature Modules
Each `feature/` module owns a vertical slice of functionality:
Module | Responsibility |
--------|---------------|
`feature:intro` | Onboarding/welcome flow |
`feature:messaging` | Messages, channels, contacts, quick chat |
`feature:connections` | Bluetooth/USB/TCP connection management |
`feature:map` | Map display, waypoints |
`feature:node` | Node list, node detail, metrics |
`feature:settings` | All configuration screens |
`feature:firmware` | Firmware update flow |
`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
- Depend on `core` modules, never on other `feature` modules
- Own their navigation entries and DI registrations
- Contain platform-specific implementations in `androidMain`/`jvmMain`/`iosMain`
### `core/*` — Core Modules
Shared infrastructure used by all features:
Module | Responsibility |
--------|---------------|
`core:common` | Utilities, extensions, build config |
`core:navigation` | Routes, deep links, Navigation 3 |
`core:ui` | Shared Compose components, icons, theme |
`core:resources` | Shared string resources |
`core:model` | Domain models |
`core:data` | Data layer abstractions |
`core:domain` | Use cases / business logic |
`core:database` | Room KMP database |
`core:datastore` | DataStore preferences |
`core:prefs` | App preferences |
`core:repository` | Repository interfaces |
`core:service` | Mesh service layer |
`core:di` | DI utilities |
`core:network` | HTTP/serial/transport |
`core:ble` | Bluetooth LE abstractions |
`core:barcode` | QR / barcode scanning (channel-share QR codes) |
`core:nfc` | NFC read/write support |
`core:takserver` | Embedded TAK server integration |
`core:testing` | Test utilities |
`core:konsist` | Konsist architecture/convention tests |
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:
```
src/
├── commonMain/ ← Shared code (all platforms)
├── commonTest/ ← Shared tests
├── androidMain/ ← Android-specific
├── jvmMain/ ← Desktop JVM-specific
├── iosMain/ ← iOS-specific
└── jvmTest/ ← Desktop test host
```
**Golden Rules:**
- No `android.*` imports in `commonMain`
- Platform-specific code goes in appropriate source set
- Prefer interfaces + DI over `expect`/`actual` for complex behaviors
- Use `expect`/`actual` only for simple declarations
## Dependency Injection
The project uses **Koin** with annotation processing:
- `@Module`, `@Single`, `@Factory` annotations
- `@ComponentScan` for automatic registration
- Feature modules export their own `Feature*Module` class
- App/Desktop compose all modules in their root DI configuration
## Radio Control
Features issue radio commands through `RadioController` (`core:repository`), a composite of four
focused sub-interfaces so callers can depend on just the slice they need:
Sub-interface | Responsibility |
---------------|---------------|
`AdminController` | Config, channels, owner, device lifecycle, `editSettings { }` transactions |
`MessagingController` | Send packets, reactions, shared contacts |
`NodeController` | Favorite, ignore, mute, remove nodes |
`QueryController` | Telemetry, traceroute, position/user-info queries |
`RadioControllerImpl` (`core:service`) is the in-process composition root for all targets
(Desktop, iOS, single-process Android). It assembles the four sub-controllers via Kotlin interface
delegation and adds the cross-cutting concerns (connection state, packet-id, location,
device-address switching). Commands are direct suspend calls; admin writes are fire-and-forget
because the device is the source of truth (local persistence is an optimistic cache). The layered
shape mirrors the [meshtastic-sdk](https://github.com/meshtastic/meshtastic-sdk)
`AdminApi`/`TelemetryApi` design to ease a future SDK migration.
## Service Repository
`ServiceRepository` is the reactive bridge between the mesh service and all feature/UI layers.
It is decomposed into focused provider interfaces following the Interface Segregation Principle:
Interface | Responsibility |
-----------|---------------|
`ConnectionStateProvider` | Read-only `connectionState: StateFlow&lt;ConnectionState&gt;` |
`TracerouteResponseProvider` | Traceroute response state + clear |
`NeighborInfoResponseProvider` | Neighbor info response state + clear |
`ServiceStateWriter` | Write-side for handlers (set*, emit*, clear*) |
`ServiceRepository` extends all four interfaces — consumers inject the narrowest interface
they actually need. For example, `ContactsViewModel` injects only `ConnectionStateProvider`
rather than the entire `ServiceRepository`, preventing accidental access to write operations
from UI code. `RadioController` also extends `ConnectionStateProvider` so VMs that already
inject a controller sub-interface can read connection state without a separate dependency.
## Navigation
Navigation uses **Navigation 3** with typed routes:
- All routes defined in `core/navigation/Routes.kt`
- Routes are `@Serializable` data classes/objects
- Deep links resolved through `DeepLinkRouter`
- Each feature registers its own navigation entries
See [Navigation &amp; Deep Links](navigation-and-deep-links) for details.
---
</pre>
</body>
</html>
+155
View File
@@ -0,0 +1,155 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Codebase</title>
<link rel="stylesheet" href="../styles/docs.css">
</head>
<body data-page="codebase" data-locale="en">
<pre class="markdown-content"># Codebase
Repository layout, namespacing conventions, and build system overview.
## Repository Structure
```
Meshtastic-Android/
├── androidApp/ # Android application module
│ ├── src/main/ # Shared Android code
│ ├── src/google/ # Google Play flavor (Gemini, proprietary)
│ └── src/fdroid/ # F-Droid flavor (FOSS-only)
├── desktopApp/ # Desktop JVM application
├── feature/ # Feature modules (KMP)
│ ├── intro/
│ ├── messaging/
│ ├── connections/
│ ├── map/
│ ├── node/
│ ├── settings/
│ ├── firmware/
│ ├── docs/
│ ├── wifi-provision/
│ ├── widget/
│ ├── discovery/
│ └── car/
├── core/ # Core infrastructure modules (KMP)
│ ├── barcode/
│ ├── ble/
│ ├── common/
│ ├── data/
│ ├── database/
│ ├── datastore/
│ ├── di/
│ ├── domain/
│ ├── konsist/
│ ├── model/
│ ├── navigation/
│ ├── network/
│ ├── nfc/
│ ├── prefs/
│ ├── repository/
│ ├── resources/
│ ├── service/
│ ├── takserver/
│ ├── testing/
│ └── ui/
├── baselineprofile/ # Baseline Profile generation for :androidApp
├── screenshot-tests/ # Compose Preview screenshot tests (visual-regression gate)
├── docs-screenshots/ # Doc-framed composition screenshots (generate-only, not CI-gated)
├── build-logic/ # Convention plugins and build helpers
│ └── convention/
├── docs/ # Documentation source (markdown)
│ └── en/ # English source; other locales live under docs/&lt;locale&gt;/user/
│ ├── user/
│ └── developer/
├── gradle/ # Gradle wrapper and version catalog
│ └── libs.versions.toml
├── specs/ # Feature specifications
└── .github/workflows/ # CI/CD workflows
```
## Namespacing Convention
All Kotlin packages follow the pattern:
```
org.meshtastic.{layer}.{module}.{subpackage}
```
Examples:
- `org.meshtastic.core.navigation` — core navigation module
- `org.meshtastic.feature.docs.ui` — docs feature UI package
- `org.meshtastic.app.di` — app DI configuration
## Build System
### Gradle Kotlin DSL
All build files use Kotlin DSL (`.gradle.kts`). Configuration:
- **Version catalog:** `gradle/libs.versions.toml`
- **Convention plugins:** `build-logic/convention/`
- **Settings:** `settings.gradle.kts`
### Convention Plugins
Located in `build-logic/convention/src/main/kotlin/`:
Plugin | Purpose |
--------|---------|
`meshtastic.kmp.feature` | Standard feature module setup |
`meshtastic.kmp.jvm.android` | JVM + Android target configuration |
`meshtastic.kotlinx.serialization` | Serialization plugin setup |
### Build Variants (Android)
Flavor | Description |
--------|-------------|
`google` | Google Play distribution; includes proprietary APIs |
`fdroid` | F-Droid distribution; FOSS-only dependencies |
### Key Gradle Tasks
```bash
# Compile check across all KMP targets
./gradlew kmpSmokeCompile
# Run all tests
./gradlew allTests
# Code quality
./gradlew spotlessCheck detekt
# Android build
./gradlew assembleGoogleDebug assembleFdroidDebug
# Desktop run
./gradlew :desktopApp:run
# Desktop native installers for the current OS (DMG / MSI+EXE / DEB+RPM+AppImage)
./gradlew :desktopApp:packageReleaseDistributionForCurrentOS
# API reference (Dokka HTML → build/dokka/html)
./gradlew dokkaGeneratePublicationHtml
```
## Version Catalog Highlights
Key dependencies in `gradle/libs.versions.toml`:
Category | Library |
----------|---------|
Compose | Compose Multiplatform (JetBrains) |
Navigation | Navigation 3 |
DI | Koin (annotations) |
Serialization | kotlinx.serialization |
Database | Room KMP |
Networking | Ktor |
Markdown | multiplatform-markdown-renderer |
Testing | kotlin-test, compose-ui-test |
---
</pre>
</body>
</html>
+110
View File
@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Contributing</title>
<link rel="stylesheet" href="../styles/docs.css">
</head>
<body data-page="contributing" data-locale="en">
<pre class="markdown-content"># Contributing
Guidelines for contributing to the Meshtastic Android/Desktop project (a KMP codebase that also compiles for iOS).
## Branch Naming
Branches use conventional-commit style prefixes:
Prefix | Use for |
--------|---------|
`feat/&lt;scope&gt;` | New user-visible behavior |
`fix/&lt;scope&gt;` | Bug fixes |
`refactor/&lt;scope&gt;` | Code structure changes |
`chore/&lt;scope&gt;` | Tooling, deps, CI, cleanup |
`docs/&lt;scope&gt;` | Documentation only |
`build/&lt;scope&gt;` | Build system changes |
`ci/&lt;scope&gt;` | CI workflow changes |
`test/&lt;scope&gt;` | Test additions or fixes |
`deps/&lt;scope&gt;` | Dependency updates |
Timestamp-based spec prefixes (`YYYYMMDD-HHMMSS-feature-name`, as created by `/speckit.git.feature`) are also valid for spec-driven work.
Examples:
- `feat/desktop-ble-transport`
- `fix/bluetooth-reconnect`
- `20260601-074653-air-quality-telemetry`
## Development Workflow
1. **Fork** the repository (external contributors) or create a branch (maintainers).
2. **Implement** your changes following the architecture guidelines.
3. **Test** locally: `./gradlew spotlessCheck detekt kmpSmokeCompile test allTests`
4. **Commit** with clear, descriptive messages.
5. **Push** and open a Pull Request.
## Commit Messages
Follow conventional commit style:
```
feat(docs): add in-app documentation browser
fix(ble): handle reconnection timeout
refactor(navigation): migrate to typed routes
test(search): add keyword ranking tests
```
## Pull Request Checklist
Before submitting:
- [ ] Code compiles on all targets: `./gradlew kmpSmokeCompile`
- [ ] All tests pass: `./gradlew allTests`
- [ ] Code style passes: `./gradlew spotlessCheck`
- [ ] Static analysis passes: `./gradlew detekt`
- [ ] New code has appropriate test coverage
- [ ] No `android.*` imports in `commonMain`
- [ ] Koin modules registered if new DI is added
- [ ] Routes added to `Routes.kt` if new navigation is introduced
- [ ] Documentation updated if user-facing behavior changes
## Code Style
- **Formatting:** Enforced by Spotless (KtLint rules)
- **Static analysis:** Detekt with project-specific configuration
- **Imports:** No wildcard imports; organized automatically by Spotless
- **Line length:** 120 characters maximum
Run formatting:
```bash
./gradlew spotlessApply
```
## Architecture Rules
- Feature modules must not depend on other feature modules
- `commonMain` must not contain `android.*`, `java.io.*`, or platform-specific imports
- Prefer interface + DI over `expect`/`actual` for complex platform behaviors
- All navigation routes must be `@Serializable` and defined in `Routes.kt`
- Use Koin annotations (`@Single`, `@Factory`, `@Module`) for dependency injection
## Verification
Full pre-merge verification:
```bash
./gradlew spotlessCheck detekt kmpSmokeCompile test allTests
```
For docs-specific changes, also run:
```bash
./gradlew generateDocsBundle validateDocsBundle
```
## Getting Help
- [Meshtastic Discord](https://discord.gg/meshtastic) — `#app-development` channel
- GitHub Issues — for bug reports and feature requests
- GitHub Discussions — for questions and ideas
---
</pre>
</body>
</html>
+71
View File
@@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Measurement & Formatting</title>
<link rel="stylesheet" href="../styles/docs.css">
</head>
<body data-page="measurement" data-locale="en">
<pre class="markdown-content"># Measurement &amp; Formatting
How the Meshtastic Android/KMP app formats numbers, units, and locale-sensitive values.
---|---|
`MetricFormatter` | `core/common/.../util/MetricFormatter.kt` | Converts and formats physical measurements (temperature, pressure, speed, etc.) |
`NumberFormatter` | `core/common/.../util/NumberFormatter.kt` | Low-level fixed-point number formatting with locale-independent dot separator |
Both live in `org.meshtastic.core.common.util` and are available to all KMP targets (Android, Desktop, iOS).
## NumberFormatter
`NumberFormatter` provides locale-independent decimal formatting using pure arithmetic (no `String.format` or `DecimalFormat`):
```kotlin
object NumberFormatter {
fun format(value: Double, decimalPlaces: Int): String
fun format(value: Float, decimalPlaces: Int): String
}
```
&gt; **Why locale-independent?** Meshtastic is a mesh networking app where consistency matters — sensor readings shared between nodes should look the same everywhere. `NumberFormatter` always uses `.` as the decimal separator.
---|---|
`temperature` | `isFahrenheit` | `°F = °C × 1.8 + 32` |
`windSpeed` | `isImperial` | m/s × 2.23694 → mph |
`rainfall` | `isImperial` | mm ÷ 25.4 → in |
Everything else (voltage, current, pressure, SNR, RSSI, humidity, percent) displays in its native metric units. The user-facing [Units &amp; Locale](../user/units-and-locale) page explains what end users see.
## DateFormatter
Date and time formatting uses the `DateFormatter` `expect object` with platform-specific `actual` implementations:
Function | Output Example |
---|---|
`formatRelativeTime()` | "5 min ago" |
`formatDateTime()` | "May 13, 2026 2:30 PM" |
`formatShortDate()` | "May 13" |
`formatTime()` | "2:30 PM" |
`formatTimeWithSeconds()` | "2:30:45 PM" |
`formatDate()` | "2026-05-13" |
`formatDateTimeShort()` | "5/13/26 2:30 PM" |
Unlike `MetricFormatter`, `DateFormatter` is declared with `expect`/`actual` (an `expect object` in `commonMain`, an `actual object` per platform) because date formatting inherently depends on platform locale APIs.
---|
Locale-independent decimal separator (`.`) | Mesh data shared between nodes must be consistent |
Pure arithmetic formatting (no `DecimalFormat`) | Works identically on JVM, Native, and JS targets |
Only temperature, wind speed, and rainfall convert | The remaining metric units are universally understood in their native form |
`object` singleton pattern | Stateless utility — no instance management needed |
---
## Related
- **User-facing docs**: [Units &amp; Locale](../user/units-and-locale) explains what end users see
- **Source code**: `core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/MetricFormatter.kt`
- **Tests**: `core/common/src/commonTest/kotlin/org/meshtastic/core/common/util/MetricFormatterTest.kt`
</pre>
</body>
</html>
@@ -0,0 +1,148 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Navigation & Deep Links</title>
<link rel="stylesheet" href="../styles/docs.css">
</head>
<body data-page="navigation-and-deep-links" data-locale="en">
<pre class="markdown-content"># Navigation &amp; Deep Links
The app uses **Navigation 3** with typed, serializable routes and centralized deep link resolution.
## Route Architecture
All routes are defined in `core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt`.
### Route Hierarchy
```kotlin
interface Route : NavKey // All routes implement NavKey
interface Graph : Route // Graph roots for navigation hierarchies
@Serializable
sealed interface SettingsRoute : Route {
@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
// ...
}
```
### Conventions
- Routes are `@Serializable` for state restoration
- Use `data object` for routes without parameters
- Use `data class` for parameterized routes
- Group related routes under a `sealed interface`
- Graph entry points implement both the route interface and `Graph`
## Deep Link Router
`DeepLinkRouter` in `core/navigation` maps URI deep links to typed backstack lists.
### URI Format
Both forms resolve through the same `DeepLinkRouter`, so any path below works with either scheme:
```text
meshtastic://meshtastic/{path}
https://meshtastic.org/{path} # App Link, android:autoVerify — also opens in-app on a real device/adb
```
`adb shell am start -a android.intent.action.VIEW -d "meshtastic://meshtastic/{path}"` is the fastest way to
trigger any route below from a shell or automation script without touching the UI.
For the `https` form to open in-app, each top-level path segment must also be declared as an
`android:pathPrefix` in the `android:autoVerify` intent-filter in `androidApp/src/main/AndroidManifest.xml` —
otherwise the link opens in the browser. Adding a new top-level route therefore takes three steps: add the
segment to `DeepLinkRouter.topLevelPathSegments` (the router refuses to dispatch segments outside that set),
add its `when` branch in `DeepLinkRouter.route()`, and add the matching `pathPrefix` to the manifest.
`DeepLinkManifestConsistencyTest` (androidApp unit tests) checks the manifest against the set, so a missing
manifest entry fails CI.
**Source of truth:** the always-current list of top-level segments is `topLevelPathSegments` in
[`DeepLinkRouter`](https://github.com/meshtastic/Meshtastic-Android/blob/main/core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/DeepLinkRouter.kt)
— sub-paths live in the `route()` `when` block plus its helper maps (`settingsSubRoutes`, `nodeDetailSubRoutes`);
the class-level KDoc is illustrative, not exhaustive. It also exists as executable spec in
[`DeepLinkRouterTest.kt`](https://github.com/meshtastic/Meshtastic-Android/blob/main/core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/DeepLinkRouterTest.kt).
The table below is a snapshot for quick reference — check those two files if it looks out of date.
### Supported Deep Links
URI Path | Route | Notes |
----------|-------|-------|
`/connections` | `ConnectionsRoute.Connections(null)` | Connections screen |
`/connections?address={prefixedAddress}` | `ConnectionsRoute.Connections(address)` | Auto-connects to a device without manual selection — the address uses the app's internal transport-prefixed format: `t192.168.1.1:4403` (TCP), `xAA:BB:CC:DD:EE:FF` (BLE), `s/dev/ttyUSB0` (serial). Intended for scripts/AI tooling driving the app. |
`/connections?address=n` | `ConnectionsRoute.Connections("n")` | Disconnects the current device instead of connecting (`n` = the internal "no device selected" sentinel). |
`/wifi-provision` | `WifiProvisionRoute.WifiProvision(null)` | WiFi provisioning screen |
`/wifi-provision?address={mac}` | `WifiProvisionRoute.WifiProvision(mac)` | Provisioning targeting a specific device MAC |
`/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 |
`/discovery` | `DiscoveryRoute.DiscoveryGraph` | Local Mesh Discovery entry point |
`/settings/local-mesh-discovery/session/{sessionId}` | `DiscoveryRoute.DiscoverySummary(sessionId)` | Discovery session result |
`/nodes` | `NodesRoute.Nodes` | Node list |
`/nodes/{destNum}` | `NodesRoute.NodeDetail(destNum)` | Node detail |
`/nodes/{destNum}/{metric}` | e.g. `NodeDetailRoute.DeviceMetrics(destNum)` | Specific node metric tab (`device-metrics`, `signal`, `power`, `traceroute`, `pax`, `neighbors`, ...) |
`/messages` | `ContactsRoute.Contacts` | Conversation list |
`/messages/{contactKey}` | `ContactsRoute.Messages(contactKey)` | Specific conversation |
`/share?message={text}` | `ContactsRoute.Share(message)` | Share-to-contact composer |
`/quickchat` | `ContactsRoute.QuickChat` | Quick chat picker |
`/map` | `MapRoute.Map(null)` | Map view |
`/map/{waypointId}` | `MapRoute.Map(waypointId)` | Map centered on a waypoint |
`/channels` | `ChannelsRoute.Channels` | Channel list |
`/firmware` | `FirmwareRoute.FirmwareGraph` | Firmware screen |
`/firmware/update` | `FirmwareRoute.FirmwareUpdate` | Firmware update flow |
### Backstack Synthesis
Deep links synthesize a full backstack, not just the target screen:
```kotlin
// /settings/helpDocs/messages-and-channels produces:
listOf(
SettingsRoute.Settings(null),
SettingsRoute.HelpDocs,
SettingsRoute.HelpDocPage("messages-and-channels"),
)
```
This ensures the user can navigate "up" correctly.
## Adding a Deep Link
1. Define the typed route in `Routes.kt`.
2. Add the mapping in `DeepLinkRouter.settingsSubRoutes` (or equivalent for other graphs).
3. Add a test in `DeepLinkRouterTest.kt`.
4. Register the navigation entry in the appropriate feature module.
5. Update the KDoc list on `DeepLinkRouter.route()` and the table above — they're the two places tooling/agents look to discover what deep links exist.
## Navigation Entry Registration
Each feature module provides entries via an extension function:
```kotlin
fun EntryProviderScope&lt;NavKey&gt;.docsEntries(backStack: NavBackStack&lt;NavKey&gt;) {
entry&lt;SettingsRoute.HelpDocs&gt; { DocsBrowserScreen(backStack) }
entry&lt;SettingsRoute.HelpDocPage&gt; { route -&gt; DocsPageRouteScreen(route.pageId, backStack) }
}
```
These are called from the settings navigation composition.
## Testing
Deep link routing is tested in:
```
core/navigation/src/commonTest/kotlin/org/meshtastic/core/navigation/DeepLinkRouterTest.kt
```
---
</pre>
</body>
</html>
+92
View File
@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Persistence</title>
<link rel="stylesheet" href="../styles/docs.css">
</head>
<body data-page="persistence" data-locale="en">
<pre class="markdown-content"># Persistence
How the Meshtastic app stores data across different mechanisms.
## Room KMP Database
**Module:** `core:database`
The primary structured data store:
- Node information and history
- Message history
- Waypoints
- Telemetry data
- Channel configurations
### Key Points
- Uses Room KMP for cross-platform compatibility
- 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
Entity | Description |
--------|-------------|
`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 |
`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 |
`DiscoverySessionEntity` | A Local Mesh Discovery scan session (timestamp, presets scanned, home preset) |
`DiscoveryPresetResultEntity` | Per-preset result within a discovery session |
`DiscoveredNodeEntity` | Nodes found during a discovery preset scan |
`DeviceLinkEntity` | Cached `msh.to` device links from the Meshtastic API |
&gt; 💡 **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:
- Local radio configuration (LocalConfig proto)
- Module configuration (ModuleConfig proto)
- Channel set data
- Local statistics
- Recently connected device addresses
## Core Prefs
**Module:** `core:prefs`
Higher-level preferences abstraction:
- User-facing settings
- App behavior configuration
- Feature toggles
## What Docs Intentionally Skip
The `feature:docs` module uses **no** Room or persistent database. Documentation ships as build-time assets versioned with the app binary, so it stays fully offline, is replaced on each update, and needs no migration story. Optional UX state (e.g. last viewed page) could live in `core:prefs` but isn't part of the docs data model.
## Best Practices
- Use Room for structured, queryable data that changes at runtime
- Use DataStore for simple preferences and state
- Use bundled resources/assets for static content
- Never store sensitive data (keys, passwords) in plain Room tables
- Always provide migrations for schema changes
---
</pre>
</body>
</html>
+144
View File
@@ -0,0 +1,144 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Testing</title>
<link rel="stylesheet" href="../styles/docs.css">
</head>
<body data-page="testing" data-locale="en">
<pre class="markdown-content"># Testing
Testing strategy and practices for the Meshtastic KMP project.
## Test Categories
### KMP Unit Tests (`commonTest`)
Shared tests that run on all platforms:
```bash
./gradlew allTests
```
- Business logic tests
- Data model validation
- Search/ranking algorithm tests
- Route serialization tests
### Android Host Tests
Android-specific tests that run on JVM:
```bash
./gradlew test
```
- ViewModel tests
- Repository tests with Room fakes
- Android-specific integration tests
### Compose UI Tests
Compose Multiplatform UI test framework:
```kotlin
@Test
fun myScreenTest() = runComposeUiTest {
setContent { MyScreen() }
onNodeWithText("Expected").assertIsDisplayed()
}
```
Located in `commonTest` or `jvmTest` source sets.
### Screenshot Tests
Uses Android Gradle Plugin's native (layoutlib) screenshot testing framework, split across two modules:
- **`:screenshot-tests`** — the **visual-regression gate**. CI runs `validateDebugScreenshotTest` on it; reframing one of these baselines is a real diff to review. Holds atomic, dual-purpose components.
- **`:docs-screenshots`** — **generate-only**, *not* validated in CI. Holds doc-framed compositions whose framing is tuned for the docs site, so reframing a doc image never churns the regression gate.
```bash
./gradlew :screenshot-tests:updateDebugScreenshotTest # record regression goldens
./gradlew :screenshot-tests:validateDebugScreenshotTest # compare against goldens (CI gate)
./gradlew :docs-screenshots:updateDebugScreenshotTest # record doc-framed composition images
./gradlew :screenshot-tests:copyDocsScreenshots # copy doc images from BOTH modules into docs/assets
```
Rendering is host-deterministic here (layoutlib): a local `update` produces references byte-identical to CI, so locally-recorded goldens pass `validate`. See `docs/assets/screenshots/README.md` for which module a new screenshot belongs in.
### Baseline Profile / Startup Performance
The `:baselineprofile` module (#5735) generates a [Baseline Profile](https://developer.android.com/topic/performance/baselineprofiles/overview) for `:androidApp`, AOT-compiling the hot startup paths so ART doesn't pay the JIT cost on first launch. It targets the **google** flavor (the variant most users run).
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/googleRelease/generated/baselineProfiles/` and packaged into release builds via `androidx.profileinstaller`.
&gt; ⚠️ **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
```
feature/my-feature/src/
├── commonTest/kotlin/org/meshtastic/feature/myfeature/
│ ├── MyBusinessLogicTest.kt
│ └── MyModelTest.kt
└── jvmTest/kotlin/org/meshtastic/feature/myfeature/
└── MyDesktopSpecificTest.kt
```
## Testing Guidelines
### DO
- Write tests in `commonTest` when possible (runs everywhere)
- Test business logic independently from UI
- Use fakes/stubs instead of mocks where practical
- Test edge cases: empty states, error states, boundary values
- Test deep link routing in `DeepLinkRouterTest`
- Keep tests fast — no network, no disk I/O in unit tests
### DON'T
- Don't test framework behavior (Compose internals, Room queries)
- Don't create tests that depend on other feature modules
- Don't use `Thread.sleep` — use coroutine test dispatchers
- Don't rely on test execution order
## Running Tests
```bash
# All KMP tests
./gradlew allTests
# Specific module
./gradlew :feature:docs:allTests
# Code quality
./gradlew spotlessCheck detekt
# Full verification
./gradlew spotlessCheck detekt kmpSmokeCompile test allTests
```
## CI Integration
Tests run automatically on:
- Pull request creation/update
- Push to `main`
- Pre-release validation
CI runs on GitHub-hosted Ubuntu 24.04 runners (most jobs use the `ubuntu-24.04-arm` ARM runners, a few use `ubuntu-24.04`) with JDK 25 and Gradle caching.
---
</pre>
</body>
</html>
+103
View File
@@ -0,0 +1,103 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Transport</title>
<link rel="stylesheet" href="../styles/docs.css">
</head>
<body data-page="transport" data-locale="en">
<pre class="markdown-content"># Transport
Meshtastic communicates between the app and radio hardware through multiple transport mechanisms.
## Transport Abstraction
The transport layer is abstracted through interfaces defined in `core:repository` (`RadioTransport`, `RadioTransportFactory`, `RadioInterfaceService`), with the concrete transports implemented in `core:network` (`BleRadioTransport`, TCP, mock/replay) and `core:ble`. This lets the app work identically regardless of the underlying connection type.
```
App ← RadioController → Transport (BLE | Serial | TCP)
```
## Bluetooth Low Energy (BLE)
**Module:** `core:ble`
**Platforms:** Android, Desktop (JVM via Kable), iOS (planned)
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
- MTU negotiation for optimal packet sizes
### Key Classes
- `core/ble/` — BLE scanning, connection, and GATT operations
- Platform-specific implementations in `androidMain` and `jvmMain` (Kable)
## USB Serial
**Module:** `core:network`
**Platforms:** Android (OTG), Desktop
Serial communication over USB:
- Uses `usb-serial-for-android` library on Android
- Direct serial port access on Desktop (JVM)
- Probe table for supported USB vendor/product IDs
- Automatic detection when USB device is connected
### Key Classes
- Serial prober and transport factory in `core/network`
- Desktop-specific serial in `desktopApp/src/main/kotlin/.../radio/`
## TCP/IP
**Module:** `core:network`
**Platforms:** Android, Desktop (iOS: code compiles, but there's no iOS app target or `RadioTransportFactory` yet — see Transport Factory below)
Network-based transport for WiFi-enabled radios:
- TCP socket connection to radio's IP address
- Default port: 4403
- Used for development with simulated radios
- Available when BLE/USB is impractical
## Transport Factory
The `RadioTransportFactory` interface abstracts transport creation:
```kotlin
interface RadioTransportFactory {
val supportedDeviceTypes: List&lt;DeviceType&gt;
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 BLE (Kable) + USB + TCP
- **iOS:** Planned BLE + TCP
## Connection Lifecycle
1. **Discovery** — Scan for available radios (BLE scan / USB detect / manual TCP)
2. **Connection** — Establish link to selected radio
3. **Handshake** — Exchange node info and configuration
4. **Active** — Normal message exchange
5. **Disconnection** — Clean teardown or error recovery
## Adding a New Transport
1. Implement `RadioTransport` interface
2. Register in platform-specific `RadioTransportFactory`
3. Add connection UI in `feature:connections`
4. Update DI bindings for the platform
---
</pre>
</body>
</html>