docs: comprehensive documentation audit and refresh (#5572)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
James Rich
2026-05-21 18:50:01 -07:00
committed by GitHub
parent 97ce3cd27f
commit 5ec6d80f61
34 changed files with 1588 additions and 381 deletions
+3 -1
View File
@@ -55,8 +55,10 @@ Meshtastic-Android uses unit tests, Robolectric JVM tests, and instrumented UI t
- Add or update tests for any new features or bug fixes.
- Ensure all tests pass by running:
- `./gradlew test` for unit and Robolectric tests
- `./gradlew test` for unit and Robolectric tests (pure-Android modules)
- `./gradlew allTests` for KMP module tests (`core:*`, `feature:*`)
- `./gradlew connectedAndroidTest` for instrumented tests
> Both `test` **and** `allTests` must pass. `allTests` covers KMP modules; `test` covers pure-Android modules. Neither alone is sufficient.
- For UI components, write Robolectric Compose tests where possible for faster execution.
- If your change is difficult to test, explain why in your pull request.
+56 -11
View File
@@ -13,13 +13,13 @@
This is a tool for using Android (and Compose Desktop) with open-source mesh radios. For more information see our webpage: [meshtastic.org](https://www.meshtastic.org). If you are looking for the device side code, see [here](https://github.com/meshtastic/firmware).
This project is currently beta testing across various providers. 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!
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!
## Get Meshtastic
The easiest, and fastest way to get the latest beta releases is to use our [github releases](https://github.com/meshtastic/Meshtastic-Android/releases). It is recommend to use these with [Obtainium](https://github.com/ImranR98/Obtainium) to get the latest updates.
The easiest and fastest way to get the latest releases is to use our [GitHub releases](https://github.com/meshtastic/Meshtastic-Android/releases). It is recommended to use these with [Obtainium](https://github.com/ImranR98/Obtainium) to get the latest updates automatically.
Alternatively, these other providers are also available, but may be slower to update.
@@ -45,20 +45,27 @@ If you encounter any problems or have questions, [ask us on the discord](https:/
## Documentation
The project's documentation is generated with [Dokka](https://kotlinlang.org/docs/dokka-introduction.html) and hosted on GitHub Pages. It is automatically updated on every push to the `main` branch.
Both sites are deployed to GitHub Pages automatically on every push to `main`.
[**View Documentation**](https://meshtastic.github.io/Meshtastic-Android/)
| Site | URL | Contents |
|---|---|---|
| **User & Developer Docs** | [meshtastic.github.io/Meshtastic-Android](https://meshtastic.github.io/Meshtastic-Android/) | Jekyll site — user guide, developer guide, in-app doc content |
| **API Reference** | [meshtastic.github.io/Meshtastic-Android/api](https://meshtastic.github.io/Meshtastic-Android/api/) | Dokka-generated KDoc for all public APIs |
### Generating Locally
You can generate the documentation locally to preview your changes.
**User & Developer Docs (Jekyll):**
```bash
./gradlew generateDocsBundle publishDocsSite
BUNDLE_GEMFILE=docs/Gemfile bundle exec jekyll serve \
--source build/_site --baseurl ""
```
1. **Run the Dokka task:**
```bash
./gradlew dokkaGeneratePublicationHtml
```
2. **View the output:**
The generated HTML files will be located in the `build/dokka/html` directory. You can open the `index.html` file in your browser to view the documentation.
**API Reference (Dokka):**
```bash
./gradlew dokkaGeneratePublicationHtml
# Output: build/dokka/html/index.html
```
## Architecture
@@ -74,6 +81,44 @@ The app follows modern Android development practices, built on top of a shared K
### 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.
### Module Documentation
Each module has its own README with details on its responsibilities, API surface, and internal design.
| Module | Description |
|---|---|
| [core/api](core/api/README.md) | AIDL service API for third-party integrations |
| [core/domain](core/domain/README.md) | Business-logic use cases (radio config, sessions, exports) |
| [core/repository](core/repository/README.md) | Data & infrastructure contracts (RadioTransport, NodeRepository, ServiceRepository) |
| [core/takserver](core/takserver/README.md) | Meshtastic ↔ TAK (ATAK/iTAK) bridge — CoT server & conversion |
| [core/ble](core/ble/README.md) | Multiplatform BLE transport (Kable) |
| [core/network](core/network/README.md) | Internet comms: firmware metadata, map tiles, radio transports |
| [core/data](core/data/README.md) | Repository layer — orchestrates DB, network, and service data |
| [core/database](core/database/README.md) | Room KMP local persistence |
| [core/datastore](core/datastore/README.md) | DataStore preferences |
| [core/service](core/service/README.md) | Meshtastic Android service abstractions |
| [core/navigation](core/navigation/README.md) | Type-safe Navigation 3 route model |
| [core/resources](core/resources/README.md) | Centralised CMP string & drawable resources |
| [core/model](core/model/README.md) | Shared domain models |
| [core/ui](core/ui/README.md) | Shared UI components |
| [core/common](core/common/README.md) | Common utilities |
| [core/di](core/di/README.md) | Koin DI modules |
| [core/testing](core/testing/README.md) | Shared test fakes & utilities |
| [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 |
| [feature/settings](feature/settings/README.md) | Settings UI feature |
| [feature/firmware](feature/firmware/README.md) | Firmware update UI feature |
| [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/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) |
## Translations
You can help translate the app into your native language using [Crowdin](https://crowdin.meshtastic.org/android).
+5 -5
View File
@@ -8,7 +8,7 @@ The entire release process is managed by a single, manually-triggered GitHub Act
- **Trigger:** To start a new release or promote an existing one, a developer manually runs the workflow from the GitHub Actions tab.
- **Inputs:** The workflow requires the following inputs:
1. `version`: The base version number you are releasing (e.g., `2.4.0`).
1. `version`: The base version number you are releasing (e.g., `2.7.0`).
2. `channel`: The release channel you are targeting (`internal`, `closed`, `open`, or `production`).
3. `build_desktop`: Whether to build and attach Desktop native installers (default: `false`).
- **Automation:** The workflow handles everything automatically:
@@ -27,14 +27,14 @@ The entire release process is managed by a single, manually-triggered GitHub Act
1. Navigate to the **Actions** tab in the GitHub repository.
2. Select the **`Create or Promote Release`** workflow.
3. Click the **"Run workflow"** dropdown.
4. Enter the base `version` (e.g., `2.4.0`).
4. Enter the base `version` (e.g., `2.7.0`).
5. Select the `internal` channel.
6. Check **`build_desktop`** if you want Desktop installers included in this release.
7. Click **"Run workflow"**.
The workflow will:
1. **Create a new commit** on the current branch containing updated assets, translations, and the new changelog.
2. **Tag** that commit with an incremental internal tag (e.g., `v2.4.0-internal.1`).
2. **Tag** that commit with an incremental internal tag (e.g., `v2.7.0-internal.1`).
3. **Build & Deploy** the verified Android artifact to the Play Store Internal track.
4. **Build Desktop** *(if enabled)* native installers on macOS, Windows, and Linux runners.
5. Publish a **draft** pre-release on GitHub with all artifacts attached.
@@ -45,7 +45,7 @@ Once an internal build has been verified, you can promote it to a wider audience
1. Run the **`Create or Promote Release`** workflow again with the same base `version`.
2. Select the next channel in the sequence (e.g., `closed`, then `open`).
3. The workflow will create a new incremental tag for that channel (e.g., `v2.4.0-closed.1`) and create a **published** pre-release on GitHub.
3. The workflow will create a new incremental tag for that channel (e.g., `v2.7.0-closed.1`) and create a **published** pre-release on GitHub.
### 3. Promote to Production
@@ -54,7 +54,7 @@ After testing is complete on all pre-release channels, you can create the final
1. Run the **`Create or Promote Release`** workflow one last time.
2. Use the same base `version`.
3. Select the `production` channel.
4. The workflow will create a clean version tag (e.g., `v2.4.0`) and create a **published, stable** (non-prerelease) release on GitHub.
4. The workflow will create a clean version tag (e.g., `v2.7.0`) and create a **published, stable** (non-prerelease) release on GitHub.
### 4. Post-Release
@@ -27,11 +27,18 @@ import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.kotlin.dsl.register
import java.io.File
private const val DEFAULT_NAV_ORDER = 999
private const val MIN_KEYWORD_LENGTH = 3
private const val MAX_KEYWORDS = 30
private const val BYTES_PER_MB = 1024.0 * 1024.0
private const val BUNDLE_SIZE_HARD_LIMIT_MB = 10.0
private const val BUNDLE_SIZE_WARN_THRESHOLD_MB = 8.0
private val LOCALE_PATTERN = Regex("^[a-z]{2,3}(-r[A-Z]{2})?$")
/**
* Registers docs generation, validation, and publishing tasks.
*
@@ -60,8 +67,9 @@ class DocsTasks : Plugin<Project> {
dependsOn("generateDocsBundle")
bundleDir.set(outputDir.map { it.dir("common") })
schemaFile.set(
project.rootProject.layout.projectDirectory
.file("specs/20260507-161858-app-docs-markdown/contracts/keyword-index-schema.json")
project.rootProject.layout.projectDirectory.file(
"specs/20260507-161858-app-docs-markdown/contracts/keyword-index-schema.json",
),
)
}
@@ -78,18 +86,26 @@ class DocsTasks : Plugin<Project> {
}
}
private data class IndexEntry(
val id: String,
val title: String,
val section: String,
val locale: String,
val resourcePath: String,
val navOrder: Int,
val keywords: List<String>,
val aliases: List<String>,
val charCount: Int,
)
abstract class GenerateDocsBundleTask : DefaultTask() {
@get:InputDirectory
abstract val sourceDir: DirectoryProperty
@get:InputDirectory abstract val sourceDir: DirectoryProperty
@get:OutputDirectory
abstract val generatedOutputDir: DirectoryProperty
@get:OutputDirectory abstract val generatedOutputDir: DirectoryProperty
@get:Input
abstract val channel: Property<String>
@get:Input abstract val channel: Property<String>
@get:Input
abstract val version: Property<String>
@get:Input abstract val version: Property<String>
@TaskAction
fun generate() {
@@ -97,170 +113,149 @@ abstract class GenerateDocsBundleTask : DefaultTask() {
val out = generatedOutputDir.get().asFile
out.mkdirs()
val indexEntries = mutableListOf<String>()
var pageCount = 0
val entries = processEnglishSources(src, out) + processLocaleSources(src, out)
writeIndexJson(out, entries)
writeCss(out)
writeLocalesManifest(src, out)
// Process English user and developer directories (under docs/en/)
val enDir = File(src, "en")
listOf("user", "developer").forEach { section ->
val sectionDir = File(enDir, section)
if (!sectionDir.exists()) return@forEach
val localeCount =
src.listFiles { f -> f.isDirectory && LOCALE_PATTERN.matches(f.name) && f.name != "en" }?.size ?: 0
logger.lifecycle(
"Generated docs bundle: ${entries.size} pages ($localeCount locales), " +
"channel=${channel.get()}, version=${version.get()}",
)
}
sectionDir.listFiles { f -> f.extension == "md" }?.sortedBy { it.name }?.forEach { mdFile ->
private fun processEnglishSources(src: File, out: File): List<IndexEntry> =
listOf("user", "developer").flatMap { section ->
val sectionDir = File(File(src, "en"), section)
if (!sectionDir.exists()) {
return@flatMap emptyList()
}
sectionDir
.listFiles { f -> f.extension == "md" }
?.sortedBy { it.name }
?.map { processMarkdown(it, section, "en", out) } ?: emptyList()
}
private fun processLocaleSources(src: File, out: File): List<IndexEntry> =
src.listFiles { f -> f.isDirectory && LOCALE_PATTERN.matches(f.name) && f.name != "en" }
?.sortedBy { it.name }
?.flatMap { localeDir ->
val sectionDir = File(localeDir, "user")
if (!sectionDir.exists()) {
return@flatMap emptyList()
}
sectionDir
.listFiles { f -> f.extension == "md" }
?.sortedBy { it.name }
?.map { processMarkdown(it, "user", localeDir.name, out) } ?: emptyList()
} ?: emptyList()
private fun processMarkdown(mdFile: File, section: String, locale: String, out: File): IndexEntry {
val frontmatter = parseFrontmatter(mdFile)
val id = mdFile.nameWithoutExtension
val title = frontmatter["title"] ?: id.replace("-", " ").replaceFirstChar { it.uppercase() }
val navOrder = frontmatter["nav_order"]?.toIntOrNull() ?: 999
val aliases = parseListField(frontmatter["aliases_raw"] ?: "")
val keywords = extractKeywords(mdFile, title)
val charCount = mdFile.readText().length
// Generate simple HTML wrapper
val htmlDir = File(out, "docs/$section")
htmlDir.mkdirs()
val htmlFile = File(htmlDir, "$id.html")
htmlFile.writeText(generateHtml(mdFile, title, "en"))
// Build index entry
val keywordsJson = keywords.joinToString(", ") { "\"$it\"" }
val aliasesJson = aliases.joinToString(", ") { "\"$it\"" }
indexEntries.add("""
| {
| "id": "$id",
| "title": "$title",
| "section": "$section",
| "locale": "en",
| "resourcePath": "docs/$section/$id.html",
| "navOrder": $navOrder,
| "keywords": [$keywordsJson],
| "aliases": [$aliasesJson],
| "charCount": $charCount
| }
""".trimMargin())
pageCount++
}
val resourcePath = if (locale == "en") "docs/$section/$id.html" else "docs/$locale/$section/$id.html"
File(out, resourcePath).also { it.parentFile.mkdirs() }.writeText(generateHtml(mdFile, title, locale))
return IndexEntry(
id = id,
title = title,
section = section,
locale = locale,
resourcePath = resourcePath,
navOrder = frontmatter["nav_order"]?.toIntOrNull() ?: DEFAULT_NAV_ORDER,
keywords = extractKeywords(mdFile, title),
aliases = parseListField(frontmatter["aliases_raw"] ?: ""),
charCount = mdFile.readText().length,
)
}
// Process Crowdin locale directories: docs/{qualifier}/user/*.md
// Crowdin %android_code% produces: fr, pt-rBR, zh-rCN, zh-rTW
// Skip "en" since English sources are handled above.
val localePattern = Regex("^[a-z]{2,3}(-r[A-Z]{2})?$")
src.listFiles { f -> f.isDirectory && localePattern.matches(f.name) && f.name != "en" }
?.sortedBy { it.name }
?.forEach { localeDir ->
val locale = localeDir.name
listOf("user").forEach { section ->
val localeSectionDir = File(localeDir, section)
if (!localeSectionDir.exists()) return@forEach
localeSectionDir.listFiles { f -> f.extension == "md" }?.sortedBy { it.name }?.forEach { mdFile ->
val frontmatter = parseFrontmatter(mdFile)
val id = mdFile.nameWithoutExtension
val title = frontmatter["title"]
?: id.replace("-", " ").replaceFirstChar { it.uppercase() }
val navOrder = frontmatter["nav_order"]?.toIntOrNull() ?: 999
val keywords = extractKeywords(mdFile, title)
val charCount = mdFile.readText().length
// Generate locale-qualified HTML
val htmlDir = File(out, "docs/$locale/$section")
htmlDir.mkdirs()
val htmlFile = File(htmlDir, "$id.html")
htmlFile.writeText(generateHtml(mdFile, title, locale))
// Build locale index entry
val keywordsJson = keywords.joinToString(", ") { "\"$it\"" }
indexEntries.add("""
| {
| "id": "$id",
| "title": "$title",
| "section": "$section",
| "locale": "$locale",
| "resourcePath": "docs/$locale/$section/$id.html",
| "navOrder": $navOrder,
| "keywords": [$keywordsJson],
| "aliases": [],
| "charCount": $charCount
| }
""".trimMargin())
pageCount++
}
private fun writeIndexJson(out: File, entries: List<IndexEntry>) {
val json =
entries.joinToString(",\n", "[\n", "\n]") { e ->
val kw = e.keywords.joinToString(", ") { "\"$it\"" }
val al = e.aliases.joinToString(", ") { "\"$it\"" }
""" {"id":"${e.id}","title":"${e.title}","section":"${e.section}",""" +
""""locale":"${e.locale}","resourcePath":"${e.resourcePath}",""" +
""""navOrder":${e.navOrder},"keywords":[$kw],"aliases":[$al],"charCount":${e.charCount}}"""
}
File(out, "index.json").writeText(json)
}
// Write index.json
val indexFile = File(out, "index.json")
indexFile.writeText("[\n${indexEntries.joinToString(",\n")}\n]")
// Write shared CSS
val cssDir = File(out, "docs/styles")
cssDir.mkdirs()
private fun writeCss(out: File) {
val cssDir = File(out, "docs/styles").also { it.mkdirs() }
File(cssDir, "docs.css").writeText(generateCss())
// Write locales manifest (for consumers that need to know available translations)
val localesManifest = src.listFiles { f -> f.isDirectory && localePattern.matches(f.name) && f.name != "en" }
?.map { it.name }?.sorted() ?: emptyList()
val manifestFile = File(out, "locales.json")
manifestFile.writeText(localesManifest.joinToString(", ", "[", "]") { "\"$it\"" })
logger.lifecycle("Generated docs bundle: $pageCount pages (${localesManifest.size} locales), channel=${channel.get()}, version=${version.get()}")
}
private fun parseFrontmatter(file: File): Map<String, String> {
private fun writeLocalesManifest(src: File, out: File) {
val locales =
src.listFiles { f -> f.isDirectory && LOCALE_PATTERN.matches(f.name) && f.name != "en" }
?.map { it.name }
?.sorted() ?: emptyList()
File(out, "locales.json").writeText(locales.joinToString(", ", "[", "]") { "\"$it\"" })
}
}
private fun parseFrontmatter(file: File): Map<String, String> {
val lines = file.readLines()
if (lines.firstOrNull()?.trim() != "---") return emptyMap()
val endIndex = lines.drop(1).indexOfFirst { it.trim() == "---" }
if (endIndex < 0) return emptyMap()
val fmLines = lines.subList(1, endIndex + 1)
val endIndex =
lines
.takeIf { it.firstOrNull()?.trim() == "---" }
?.drop(1)
?.indexOfFirst { it.trim() == "---" }
?.takeIf { it >= 0 } ?: return emptyMap()
val result = mutableMapOf<String, String>()
var inAliases = false
val aliasesBuilder = StringBuilder()
for (line in fmLines) {
if (line.startsWith("aliases:")) {
inAliases = true
continue
}
if (inAliases) {
if (line.startsWith(" - ")) {
aliasesBuilder.append(line.removePrefix(" - ").trim()).append(",")
} else {
for (line in lines.subList(1, endIndex + 1)) {
when {
line.startsWith("aliases:") -> inAliases = true
inAliases && line.startsWith(" - ") -> aliasesBuilder.append(line.removePrefix(" - ").trim()).append(",")
inAliases -> {
inAliases = false
result["aliases_raw"] = aliasesBuilder.toString().trimEnd(',')
if (line.contains(":")) result += parseLine(line)
}
}
if (!inAliases && line.contains(":")) {
val (key, value) = line.split(":", limit = 2)
result[key.trim()] = value.trim()
!inAliases && line.contains(":") -> result += parseLine(line)
}
}
if (inAliases) result["aliases_raw"] = aliasesBuilder.toString().trimEnd(',')
return result
}
}
private fun parseListField(raw: String): List<String> =
raw.split(",").map { it.trim() }.filter { it.isNotEmpty() }
private fun parseLine(line: String): Map<String, String> {
val (key, value) = line.split(":", limit = 2)
return mapOf(key.trim() to value.trim())
}
private fun extractKeywords(file: File, title: String): List<String> {
private fun parseListField(raw: String): List<String> = raw.split(",").map { it.trim() }.filter { it.isNotEmpty() }
private fun extractKeywords(file: File, title: String): List<String> {
val text = file.readText().lowercase()
val keywords = mutableSetOf<String>()
// Add title words
title.lowercase().split(Regex("[^a-z0-9]+")).filter { it.length >= 3 }.forEach { keywords.add(it) }
// Extract headings
title.lowercase().split(Regex("[^a-z0-9]+")).filter { it.length >= MIN_KEYWORD_LENGTH }.forEach { keywords.add(it) }
Regex("^#{1,3}\\s+(.+)$", RegexOption.MULTILINE).findAll(text).forEach { match ->
match.groupValues[1].split(Regex("[^a-z0-9]+")).filter { it.length >= 3 }.forEach { keywords.add(it) }
}
return keywords.toList().take(30)
match.groupValues[1]
.split(Regex("[^a-z0-9]+"))
.filter { it.length >= MIN_KEYWORD_LENGTH }
.forEach { keywords.add(it) }
}
return keywords.toList().take(MAX_KEYWORDS)
}
private fun generateHtml(mdFile: File, title: String, locale: String = "en"): String {
val content = mdFile.readText()
// Strip frontmatter
private fun generateHtml(mdFile: File, title: String, locale: String = "en"): String {
val content =
mdFile
.readText()
.replace(Regex("^---[\\s\\S]*?---\\s*", RegexOption.MULTILINE), "")
.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
val dir = if (locale == "ar") "rtl" else "ltr"
// Locale pages are one level deeper: docs/{locale}/user/foo.html vs docs/en/user/foo.html
val cssPath = if (locale != "en") "../../styles/docs.css" else "../styles/docs.css"
return """
|<!DOCTYPE html>
@@ -275,10 +270,12 @@ abstract class GenerateDocsBundleTask : DefaultTask() {
|<pre class="markdown-content">$content</pre>
|</body>
|</html>
""".trimMargin()
}
"""
.trimMargin()
}
private fun generateCss(): String = """
private fun generateCss(): String =
"""
|:root {
| --primary: #2C2D3C;
| --accent: #67EA94;
@@ -310,77 +307,76 @@ abstract class GenerateDocsBundleTask : DefaultTask() {
|.callout-info { border-left: 4px solid var(--info); padding: 12px; background: #E8EAF6; margin: 12px 0; }
|.callout-warning { border-left: 4px solid var(--warning); padding: 12px; background: #FFF3E0; margin: 12px 0; }
|.callout-error { border-left: 4px solid var(--error); padding: 12px; background: #FDEAEA; margin: 12px 0; }
""".trimMargin()
}
"""
.trimMargin()
abstract class ValidateDocsBundleTask : DefaultTask() {
@get:InputDirectory
@get:Optional
@get:InputDirectory @get:Optional
abstract val bundleDir: DirectoryProperty
@get:InputFile
@get:Optional
@get:InputFile @get:Optional
abstract val schemaFile: RegularFileProperty
@TaskAction
fun validate() {
val dir = bundleDir.get().asFile
val indexFile = File(dir, "index.json")
val errors = mutableListOf<String>()
// Check index exists
if (!indexFile.exists()) {
throw org.gradle.api.GradleException("index.json not found in ${dir.absolutePath}")
errors.add("index.json not found in ${dir.absolutePath}")
}
// Check index is valid JSON array
val indexContent = indexFile.readText()
if (!indexContent.trimStart().startsWith("[")) {
throw org.gradle.api.GradleException("index.json must be a JSON array")
val indexContent = if (indexFile.exists()) indexFile.readText() else ""
if (indexContent.isNotEmpty() && !indexContent.trimStart().startsWith("[")) {
errors.add("index.json must be a JSON array")
}
// Check bundle size
val totalSize = dir.walkTopDown().filter { it.isFile }.sumOf { it.length() }
val sizeMb = totalSize / (1024.0 * 1024.0)
if (sizeMb > 10.0) {
throw org.gradle.api.GradleException("Bundle size ${String.format("%.2f", sizeMb)} MB exceeds 10 MB hard limit")
}
if (sizeMb > 8.0) {
logger.warn("WARNING: Bundle size ${String.format("%.2f", sizeMb)} MB exceeds 8 MB warning threshold")
val sizeMb = dir.walkTopDown().filter { it.isFile }.sumOf { it.length() } / BYTES_PER_MB
if (sizeMb > BUNDLE_SIZE_HARD_LIMIT_MB) {
errors.add(
"Bundle size ${String.format("%.2f", sizeMb)} MB exceeds $BUNDLE_SIZE_HARD_LIMIT_MB MB hard limit",
)
} else if (sizeMb > BUNDLE_SIZE_WARN_THRESHOLD_MB) {
logger.warn(
"Bundle size ${String.format(
"%.2f",
sizeMb,
)} MB exceeds $BUNDLE_SIZE_WARN_THRESHOLD_MB MB warning threshold",
)
}
// Check all referenced pages exist
val pagePattern = Regex("\"resourcePath\"\\s*:\\s*\"([^\"]+)\"")
val referencedPaths = pagePattern.findAll(indexContent).map { it.groupValues[1] }.toList()
val missingPages = referencedPaths.filter { !File(dir, it).exists() }
if (missingPages.isNotEmpty()) {
throw org.gradle.api.GradleException("Missing page files: ${missingPages.joinToString()}")
if (indexContent.isNotEmpty()) {
val paths =
Regex("\"resourcePath\"\\s*:\\s*\"([^\"]+)\"").findAll(indexContent).map { it.groupValues[1] }.toList()
paths.filter { !File(dir, it).exists() }.forEach { errors.add("Missing page file: $it") }
if (errors.isEmpty()) {
logger.lifecycle(
"Docs bundle validation PASSED: ${paths.size} pages, ${String.format("%.2f", sizeMb)} MB",
)
}
}
logger.lifecycle("Docs bundle validation PASSED: ${referencedPaths.size} pages, ${String.format("%.2f", sizeMb)} MB")
if (errors.isNotEmpty()) throw org.gradle.api.GradleException(errors.joinToString("\n"))
}
}
abstract class PublishDocsSiteTask : DefaultTask() {
@get:InputDirectory
abstract val sourceDir: DirectoryProperty
@get:InputDirectory abstract val sourceDir: DirectoryProperty
@get:InputDirectory
abstract val bundleDir: DirectoryProperty
@get:InputDirectory abstract val bundleDir: DirectoryProperty
@get:OutputDirectory
abstract val siteOutputDir: DirectoryProperty
@get:OutputDirectory abstract val siteOutputDir: DirectoryProperty
@get:Input
abstract val channel: Property<String>
@get:Input abstract val channel: Property<String>
@get:Input
abstract val version: Property<String>
@get:Input abstract val version: Property<String>
@TaskAction
fun publish() {
val siteDir = siteOutputDir.get().asFile
val channelPath = when (channel.get()) {
val channelPath =
when (channel.get()) {
"release" -> "v${version.get()}"
"root" -> ""
else -> channel.get()
@@ -388,23 +384,21 @@ abstract class PublishDocsSiteTask : DefaultTask() {
val outDir = if (channelPath.isEmpty()) siteDir else File(siteDir, channelPath)
outDir.mkdirs()
// Copy generated bundle to site output
val bundle = bundleDir.get().asFile
bundle.copyRecursively(outDir, overwrite = true)
bundleDir.get().asFile.copyRecursively(outDir, overwrite = true)
// Copy source markdown for Jekyll processing
val src = sourceDir.get().asFile
src.listFiles()?.filter { it.name != "_site" && it.name != ".jekyll-cache" }?.forEach { f ->
if (f.isDirectory) f.copyRecursively(File(outDir, f.name), overwrite = true)
else f.copyTo(File(outDir, f.name), overwrite = true)
sourceDir
.get()
.asFile
.listFiles()
?.filter { it.name != "_site" && it.name != ".jekyll-cache" }
?.forEach { f ->
if (f.isDirectory) {
f.copyRecursively(File(outDir, f.name), overwrite = true)
} else {
f.copyTo(File(outDir, f.name), overwrite = true)
}
}
logger.lifecycle("Published docs site to: ${outDir.absolutePath} (channel=$channelPath)")
}
}
@@ -100,7 +100,9 @@ internal enum class PluginType(val id: String, val ref: String, val style: Strin
fun Project.configureGraphTasks() {
if (!buildFile.exists()) return
val supportedConfigurations =
// Android uses bare "api"/"implementation"; KMP uses source-set-prefixed variants
// ("commonMainApi", "androidMainImplementation", etc.) — match both by suffix.
val explicitConfigs =
providers
.gradleProperty("graph.supportedConfigurations")
.map { it.split(",").toSet() }
@@ -117,10 +119,16 @@ fun Project.configureGraphTasks() {
val deps = mutableMapOf<String, Set<Pair<String, String>>>()
val projectDeps = mutableSetOf<Pair<String, String>>()
configurations
.filter { it.name in supportedConfigurations.get() }
.filter { config ->
config.name in explicitConfigs.get() ||
config.name.endsWith("Api") ||
config.name.endsWith("Implementation")
}
.forEach { config ->
config.dependencies.withType<ProjectDependency>().forEach { dep ->
projectDeps.add(config.name to dep.path)
// Normalise to "api" or "implementation" for edge-type rendering.
val isApi = config.name == "api" || config.name.endsWith("Api")
projectDeps.add((if (isApi) "api" else "implementation") to dep.path)
}
}
deps[targetProjectPath] = projectDeps
+27 -23
View File
@@ -1,28 +1,5 @@
# `:core:ble`
## Module dependency graph
<!--region graph-->
```mermaid
graph TB
:core:ble[ble]:::kmp-library
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef compose-desktop-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000;
classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library-compose fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;
```
<!--endregion-->
## Overview
The `:core:ble` module contains the foundation for Bluetooth Low Energy (BLE) communication in the Meshtastic Android app. It uses the **Kable** multiplatform BLE library to provide a unified, Coroutine-based architecture across all supported targets (Android, Desktop, and future iOS).
@@ -81,3 +58,30 @@ The module follows a clean multiplatform architecture approach:
## Testing
The module includes unit tests for key components, utilizing Kable's architecture and standard coroutine testing tools to ensure logic correctness.
## Dependency Graph
<!--region graph-->
```mermaid
graph TB
:core:ble[ble]:::kmp-library
:core:ble -.-> :core:common
:core:ble -.-> :core:di
:core:ble -.-> :core:model
:core:ble -.-> :core:testing
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef compose-desktop-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000;
classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library-compose fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;
```
<!--endregion-->
+5 -1
View File
@@ -1,6 +1,9 @@
# `:core:common`
## Overview
**Targets:** Android · JVM (Desktop) · iOS
The `:core:common` module contains low-level utility functions, extensions, and common data structures that do not depend on any other Meshtastic-specific modules. It is designed to be highly reusable across the project.
## Key Components
@@ -17,7 +20,8 @@ Centralized utility for display strings — temperature, voltage, current, perce
### 3. `BuildConfigProvider.kt`
An interface for accessing build-time configuration in a multiplatform-friendly way.
## Module dependency graph
## Dependency Graph
<!--region graph-->
```mermaid
+13 -1
View File
@@ -13,12 +13,24 @@ The `:core:data` module implements the Repository pattern, serving as the primar
### 2. Data Sources
Internal components that handle raw data fetching from APIs or disk.
## Module dependency graph
## Dependency Graph
<!--region graph-->
```mermaid
graph TB
:core:data[data]:::kmp-library
:core:data --> :core:repository
:core:data -.-> :core:common
:core:data -.-> :core:database
:core:data -.-> :core:datastore
:core:data -.-> :core:di
:core:data -.-> :core:model
:core:data -.-> :core:network
:core:data -.-> :core:prefs
:core:data -.-> :core:proto
:core:data -.-> :core:takserver
:core:data -.-> :core:testing
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
+8 -1
View File
@@ -20,12 +20,19 @@ The `NodeInfoDao` implements specific logic to protect against impersonation and
- **Wipe Protection**: Receiving an `is_licensed=true` packet (which normally clears the public key for compliance) will **not** clear an existing valid public key if one is already known. This prevents attackers from sending fake licensed packets to wipe keys from the DB.
- **Conflict Detection**: If a new key arrives for an existing node ID that conflicts with a known valid key, the key is set to `ERROR_BYTE_STRING` to flag the potential impersonation.
## Module dependency graph
## Dependency Graph
<!--region graph-->
```mermaid
graph TB
:core:database[database]:::kmp-library
:core:database --> :core:common
:core:database --> :core:model
:core:database -.-> :core:di
:core:database -.-> :core:proto
:core:database -.-> :core:resources
:core:database -.-> :core:testing
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
+8 -1
View File
@@ -1,6 +1,9 @@
# `:core:datastore`
## Overview
**Targets:** Android · JVM (Desktop) · iOS
The `:core:datastore` module manages structured, asynchronous data storage using **Jetpack DataStore**. It is primarily used for storing complex configuration objects like radio channel sets and local device configurations.
## Key Components
@@ -13,12 +16,16 @@ The `:core:datastore` module manages structured, asynchronous data storage using
### 2. Serializers
Uses **Kotlin Serialization** to convert between Protobuf/JSON and the underlying DataStore storage.
## Module dependency graph
## Dependency Graph
<!--region graph-->
```mermaid
graph TB
:core:datastore[datastore]:::kmp-library
:core:datastore -.-> :core:common
:core:datastore -.-> :core:model
:core:datastore -.-> :core:proto
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
+6 -1
View File
@@ -1,6 +1,9 @@
# `:core:di`
## Overview
**Targets:** Android · JVM (Desktop) · iOS
The `:core:di` module defines the core Koin modules and provides standard dependencies that are shared across all other modules.
## Key Components
@@ -14,12 +17,14 @@ Provides a wrapper for standard Kotlin `CoroutineDispatchers` (`IO`, `Default`,
### 3. `ProcessLifecycle.kt`
Exposes the application's global process lifecycle as a Koin binding, enabling components to react to the app entering the foreground or background.
## Module dependency graph
## Dependency Graph
<!--region graph-->
```mermaid
graph TB
:core:di[di]:::kmp-library
:core:di -.-> :core:common
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
+140
View File
@@ -0,0 +1,140 @@
# `:core:domain`
## Overview
The `:core:domain` module is the **business-logic layer** of the KMP architecture. It contains exclusively use-case classes — no UI, no platform code, no mutable state. Each use case is a thin orchestrator that coordinates one or more repository/model dependencies to fulfil a single application action.
**Targets:** Android · JVM · iOS (via `meshtastic.kmp.library` convention plugin)
## Key Responsibilities
- Orchestrate radio configuration reads/writes (config, module config, channels, owner, position)
- Manage remote-admin session lifecycle (per-node passkey negotiation)
- Application settings toggles (theme, locale, analytics, notifications, location sharing)
- Data export (CSV mesh log, profile `.zip`, security config)
- Profile and config import/install
- Node database maintenance (clean, reset, selective purge)
## Source Structure
```
src/commonMain/kotlin/org/meshtastic/core/domain/
├── di/
│ └── CoreDomainModule.kt ← Koin @Module + component scan
└── usecase/
├── session/
│ ├── EnsureRemoteAdminSessionUseCase.kt
│ ├── EnsureSessionResult.kt
│ └── ObserveRemoteAdminSessionStatusUseCase.kt
└── settings/
├── AdminActionsUseCase.kt
├── CleanNodeDatabaseUseCase.kt
├── ExportDataUseCase.kt
├── ExportProfileUseCase.kt
├── ExportSecurityConfigUseCase.kt
├── ImportProfileUseCase.kt
├── InstallProfileUseCase.kt
├── IsOtaCapableUseCase.kt
├── MeshLocationUseCase.kt
├── ProcessRadioResponseUseCase.kt
├── RadioConfigUseCase.kt
├── SetAppIntroCompletedUseCase.kt
├── SetDatabaseCacheLimitUseCase.kt
├── SetLocaleUseCase.kt
├── SetMeshLogSettingsUseCase.kt
├── SetNotificationSettingsUseCase.kt
├── SetProvideLocationUseCase.kt
├── SetThemeUseCase.kt
├── ToggleAnalyticsUseCase.kt
└── ToggleHomoglyphEncodingUseCase.kt
```
## Notable APIs
### `EnsureRemoteAdminSessionUseCase`
Ensures a per-node remote-admin passkey session exists before entering the remote admin UI. Uses a `Mutex`-guarded `inFlight` map so that double-taps coalesce onto a single `Deferred`.
```kotlin
sealed interface EnsureSessionResult {
data object AlreadyActive : EnsureSessionResult // passkey already fresh
data object Refreshed : EnsureSessionResult // metadata response arrived
data object Timeout : EnsureSessionResult // no response within 10 s
data object Disconnected : EnsureSessionResult // radio not connected
}
```
### `RadioConfigUseCase`
Radio configuration read/write operations, all returning the `packetId` for async tracking:
| Method | Description |
|---|---|
| `setOwner` / `getOwner` | Node owner info |
| `setConfig` / `getConfig` | `Config` proto (device, position, power, …) |
| `setModuleConfig` / `getModuleConfig` | `ModuleConfig` proto |
| `getChannel` / `setRemoteChannel` | Channel configuration |
| `setFixedPosition` / `removeFixedPosition` | Fixed GPS position |
| `setRingtone` / `getRingtone` | External notification ringtone |
| `setCannedMessages` / `getCannedMessages` | Canned message slots |
### `AdminActionsUseCase`
```kotlin
reboot(destNum)
shutdown(destNum)
factoryReset(destNum, isLocal) // also clears local NodeDB when isLocal = true
nodedbReset(destNum, preserveFavorites, isLocal)
```
### `ExportDataUseCase`
Streams all mesh log packets to a CSV `BufferedSink`. Columns: date, time, from, sender name/location, received location/elevation, SNR, distance, hop limit, payload.
## Dependency Graph
```
core:domain
├── core:repository (use-case interfaces & contracts)
├── core:model (domain models)
├── core:proto (Meshtastic protobuf types)
├── core:common
├── core:database
├── core:datastore
└── core:resources
```
## DI
All use cases are registered via Koin component scan on `org.meshtastic.core.domain`. No manual binding is needed — annotate a new use case with `@Single` and it is picked up automatically.
## Dependency Graph
<!--region graph-->
```mermaid
graph TB
:core:domain[domain]:::kmp-library
:core:domain -.-> :core:repository
:core:domain -.-> :core:model
:core:domain -.-> :core:proto
:core:domain -.-> :core:common
:core:domain -.-> :core:database
:core:domain -.-> :core:datastore
:core:domain -.-> :core:resources
:core:domain -.-> :core:testing
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef compose-desktop-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000;
classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library-compose fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;
```
<!--endregion-->
+6 -1
View File
@@ -26,12 +26,17 @@ implementation(projects.core.model)
- **`androidMain`**: Contains Android-specific utilities and implementations for `expect` declarations.
- **`androidUnitTest`**: Contains unit tests that require Android-specific features (like `Parcel` testing via Robolectric).
## Module dependency graph
## Dependency Graph
<!--region graph-->
```mermaid
graph TB
:core:model[model]:::kmp-library
:core:model --> :core:proto
:core:model --> :core:common
:core:model --> :core:resources
:core:model -.-> :core:testing
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
+5 -1
View File
@@ -32,12 +32,16 @@ fun openNodeDetail(backStack: NavBackStack<NavKey>, destNum: Int) {
}
```
## Module dependency graph
## Dependency Graph
<!--region graph-->
```mermaid
graph TB
:core:navigation[navigation]:::kmp-library-compose
:core:navigation -.-> :core:common
:core:navigation -.-> :core:resources
:core:navigation -.-> :core:testing
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
+11 -2
View File
@@ -13,17 +13,26 @@ The module uses **Ktor** as its primary HTTP client for high-performance, asynch
- **`DeviceHardwareRemoteDataSource`**: Fetches definitions for supported Meshtastic hardware devices.
### 3. Shared Transports
- **`BleRadioInterface`**: Multiplatform BLE transport powered by Kable.
- **`TCPInterface`**: Multiplatform TCP transport.
- **`SerialTransport`**: JVM-shared USB/Serial transport powered by jSerialComm.
- **`BaseRadioTransportFactory`**: Common factory for instantiating the KMP transports.
## Module dependency graph
> **BLE transport** lives in [`:core:ble`](../ble/README.md), not here. `BaseRadioTransportFactory` delegates to it when an address with the `x` prefix is resolved.
## Dependency Graph
<!--region graph-->
```mermaid
graph TB
:core:network[network]:::kmp-library
:core:network --> :core:repository
:core:network -.-> :core:common
:core:network -.-> :core:di
:core:network -.-> :core:model
:core:network -.-> :core:proto
:core:network -.-> :core:ble
:core:network -.-> :core:testing
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
+2 -1
View File
@@ -11,7 +11,8 @@ A Composable side-effect that manages Android NFC adapter state and listens for
### 2. `LocalNfcScannerProvider` (core:ui/commonMain)
The shared capability contract for NFC scanning, injected via `CompositionLocalProvider` from the app layer.
## Module dependency graph
## Dependency Graph
<!--region graph-->
```mermaid
+5 -1
View File
@@ -13,12 +13,16 @@ Provides named `DataStore<Preferences>` singletons for each preference domain (a
- **`UiPrefs`**: Manages UI preferences (e.g., theme selection, unit systems).
- **`MapPrefs`**: Manages mapping preferences (e.g., preferred map provider).
## Module dependency graph
## Dependency Graph
<!--region graph-->
```mermaid
graph TB
:core:prefs[prefs]:::kmp-library
:core:prefs -.-> :core:repository
:core:prefs -.-> :core:common
:core:prefs -.-> :core:di
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
+5 -1
View File
@@ -1,6 +1,9 @@
# `:core:proto`
## Overview
**Targets:** Android · JVM (Desktop) · iOS
This module contains the generated Kotlin and Java code from the Meshtastic Protobuf definitions. It uses the [Wire](https://github.com/square/wire) library for efficient and clean model generation.
## Key Components
@@ -16,7 +19,8 @@ This module is a low-level dependency for any module that needs to encode or dec
implementation(projects.core.proto)
```
## Module dependency graph
## Dependency Graph
<!--region graph-->
```mermaid
+178
View File
@@ -0,0 +1,178 @@
# `:core:repository`
## Overview
The `:core:repository` module defines the **data and infrastructure contracts** for the Meshtastic KMP architecture. It is almost entirely interfaces — concrete implementations live in `:core:service` and platform modules. Consumers receive `:core:model` and `:core:proto` transitively because both are `api()`-exported.
**Targets:** Android · JVM · iOS (via `meshtastic.kmp.library` convention plugin)
## Key Responsibilities
- Define the reactive data contracts between the long-running mesh service and all feature/UI layers
- Declare the raw hardware I/O interface (`RadioTransport`)
- Provide the mesh node database interface (`NodeRepository`)
- Expose per-node remote-admin passkey management (`SessionManager`)
- Host all packet handlers (admin, telemetry, traceroute, store-and-forward, neighbour info)
- Manage the outbound message queue, MQTT bridge, and XModem firmware transfer
- Provide the `AppWidgetUpdater` contract so the mesh service can trigger widget refreshes without depending on Android widget APIs directly
## Source Structure
```
src/
├── commonMain/kotlin/org/meshtastic/core/repository/
│ ├── RadioTransport.kt ← interface: raw hardware I/O
│ ├── ServiceRepository.kt ← interface: service ↔ UI bridge
│ ├── NodeRepository.kt ← interface: mesh node database
│ ├── SessionManager.kt ← interface: per-node passkey store
│ ├── MeshConnectionManager.kt ← interface: connection lifecycle callbacks
│ ├── AppWidgetUpdater.kt ← interface: trigger widget refresh
│ ├── LocationRepository.kt
│ ├── LocationService.kt
│ ├── CommandSender.kt
│ ├── AdminPacketHandler.kt
│ ├── FromRadioPacketHandler.kt
│ ├── MeshActionHandler.kt
│ ├── MeshConfigFlowManager.kt
│ ├── MeshConfigHandler.kt
│ ├── MeshDataHandler.kt
│ ├── MeshLocationManager.kt
│ ├── MeshLogRepository.kt
│ ├── MeshMessageProcessor.kt
│ ├── MeshRouter.kt
│ ├── MessageFilter.kt
│ ├── MessageQueue.kt
│ ├── MqttManager.kt
│ ├── NeighborInfoHandler.kt
│ ├── NodeManager.kt
│ ├── Notification.kt / NotificationManager.kt
│ ├── PacketHandler.kt / PacketRepository.kt
│ ├── QuickChatActionRepository.kt
│ ├── RadioConfigRepository.kt
│ ├── RadioInterfaceService.kt
│ ├── RadioTransportCallback.kt / RadioTransportFactory.kt
│ ├── ServiceBroadcasts.kt
│ ├── StoreForwardPacketHandler.kt
│ ├── TelemetryPacketHandler.kt
│ ├── TracerouteHandler.kt / TracerouteSnapshotRepository.kt
│ ├── XModemFile.kt / XModemManager.kt
│ ├── usecase/
│ │ └── SendMessageUseCase.kt
│ └── di/
│ └── CoreRepositoryModule.kt
├── androidMain/kotlin/ ← Android LocationRepository actual
├── iosMain/kotlin/ ← iOS LocationRepository actual
└── jvmMain/kotlin/ ← Desktop LocationRepository actual
```
## Core Interfaces
### `RadioTransport`
Raw hardware I/O contract for all physical transports (BLE, USB, TCP, Mock).
```kotlin
interface RadioTransport {
fun handleSendToRadio(p: ByteArray)
fun start()
fun keepAlive()
suspend fun close()
}
```
### `ServiceRepository`
The primary reactive bridge between the long-running mesh service and all feature/UI layers.
```kotlin
interface ServiceRepository {
val connectionState: StateFlow<ConnectionState>
val clientNotification: StateFlow<ClientNotification?>
val errorMessage: StateFlow<String?>
val connectionProgress: StateFlow<String?>
val meshPacketFlow: Flow<MeshPacket>
val tracerouteResponse: Flow<...>
val neighborInfoResponse: Flow<...>
val serviceAction: Flow<ServiceAction>
fun setConnectionState(state: ConnectionState)
fun emitMeshPacket(packet: MeshPacket)
fun onServiceAction(action: ServiceAction)
}
```
### `NodeRepository`
Reactive mesh node database. Backed by Room KMP in `:core:service`.
```kotlin
interface NodeRepository {
val myNodeInfo: StateFlow<MyNodeInfo?>
val ourNodeInfo: StateFlow<Node?>
val myId: StateFlow<String?>
val localStats: StateFlow<LocalStats>
val nodeDBbyNum: StateFlow<Map<Int, Node>>
val onlineNodeCount: Flow<Int>
val totalNodeCount: Flow<Int>
fun getNodes(sort, filter, includeUnknown, onlyOnline, onlyDirect): Flow<List<Node>>
suspend fun upsert(node: Node)
suspend fun clearNodeDB(preserveFavorites: Boolean = false)
suspend fun deleteNode(num: Int)
suspend fun insertMetadata(nodeNum: Int, metadata: DeviceMetadata)
suspend fun installConfig(mi: MyNodeInfo, nodes: List<NodeInfo>)
}
```
### `SessionManager`
Per-node remote-admin passkey store, consumed by `:core:domain`'s `EnsureRemoteAdminSessionUseCase`.
```kotlin
interface SessionManager {
fun recordSession(srcNodeNum: Int, passkey: ByteString)
fun getPasskey(destNum: Int): ByteString
fun clearAll()
val sessionRefreshFlow: Flow<Int>
fun observeSessionStatus(destNum: Int): Flow<SessionStatus>
}
```
## Dependency Graph
```
core:repository
├── api → core:model (exported to consumers)
├── api → core:proto (exported to consumers)
├── core:common
├── core:database
└── kotlinx.coroutines, kermit, androidx.paging.common
```
## Dependency Graph
<!--region graph-->
```mermaid
graph TB
:core:repository[repository]:::kmp-library
:core:repository --> :core:model
:core:repository --> :core:proto
:core:repository -.-> :core:common
:core:repository -.-> :core:database
:core:repository -.-> :core:testing
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef compose-desktop-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000;
classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library-compose fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;
```
<!--endregion-->
+3 -1
View File
@@ -19,12 +19,14 @@ import org.meshtastic.core.resources.your_string_key
Text(text = stringResource(Res.string.your_string_key))
```
## Module dependency graph
## Dependency Graph
<!--region graph-->
```mermaid
graph TB
:core:resources[resources]:::kmp-library-compose
:core:resources -.-> :core:common
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
+19 -1
View File
@@ -1,6 +1,9 @@
# `:core:service`
## Overview
**Targets:** Android · JVM (Desktop) · iOS
The `:core:service` module contains the abstractions and client-side logic for interacting with the main Meshtastic Android Service.
## Key Components
@@ -17,12 +20,27 @@ An enum representing the current state of the radio connection (`Connected`, `Di
### 4. `ServiceAction`
Defines Intent actions for starting, stopping, and interacting with the background service.
## Module dependency graph
## Dependency Graph
<!--region graph-->
```mermaid
graph TB
:core:service[service]:::kmp-library
:core:service -.-> :core:testing
:core:service --> :core:api
:core:service --> :core:repository
:core:service -.-> :core:common
:core:service -.-> :core:data
:core:service -.-> :core:database
:core:service -.-> :core:di
:core:service -.-> :core:model
:core:service -.-> :core:navigation
:core:service -.-> :core:network
:core:service -.-> :core:ble
:core:service -.-> :core:prefs
:core:service -.-> :core:proto
:core:service -.-> :core:takserver
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
+164
View File
@@ -0,0 +1,164 @@
# `:core:takserver`
## Overview
The `:core:takserver` module implements the **Meshtastic ↔ TAK (Team Awareness Kit) bridge**. It embeds an mTLS TCP server (port 8089) compatible with ATAK (Android), iTAK (iOS), and WinTAK clients, enabling mesh-networked position sharing and GeoChat with TAK-enabled devices.
**Targets:** Android · JVM (Desktop) · iOS — fully multiplatform with `expect`/`actual` splits for compression, file I/O, and the TCP server itself.
## Key Responsibilities
- Serve an mTLS TCP listener (port 8089) compatible with the CoT (Cursor-on-Target) protocol
- Convert Meshtastic protobuf packets (`TAKPacketV2`) to CoT XML events and vice versa
- Generate ATAK Data Package `.zip` exports (team contacts, map overlays)
- Compress CoT payloads using Zstd (TAK SDK format) with `expect`/`actual` platform implementations
- Buffer up to 50 CoT messages for 5 minutes when no TAK clients are connected; drain on reconnect
- Provide Crowdin-localised TAK preference XML for ATAK client provisioning
## Source Structure
```
src/
├── commonMain/kotlin/org/meshtastic/core/takserver/
│ ├── TAKServer.kt ← interface + expect createTAKServer()
│ ├── TAKServerManager.kt ← interface + TAKServerManagerImpl (offline queue)
│ ├── TAKMeshIntegration.kt ← bridges mesh service ↔ TAK server
│ ├── CoTConversion.kt ← Position/User → CoTMessage extension fns
│ ├── CoTXml.kt / CoTXmlParser.kt / CoTXmlFrameBuffer.kt
│ ├── CoTXmlDataClasses.kt
│ ├── CoTDetailStripper.kt
│ ├── TAKModels.kt ← CoTMessage, TAKClientInfo, TAKConnectionEvent
│ ├── TAKPacketConversion.kt
│ ├── TAKPacketV2Conversion.kt
│ ├── TAKDefaults.kt
│ ├── TAKDataPackageGenerator.kt
│ ├── RouteDataPackageGenerator.kt
│ ├── TAKPrefXmlDataClasses.kt
│ ├── TakV2TypeMapper.kt
│ ├── TakConversionHelpers.kt
│ ├── XmlUtils.kt
│ ├── AtakFileWriter.kt ← expect
│ ├── TakSdkCompressor.kt ← expect (Zstd TAK-SDK frame)
│ ├── TakV2Compressor.kt ← expect (Zstd TAKPacketV2 frame)
│ ├── ZipArchiver.kt ← expect
│ ├── TakFixtureLoader.kt ← expect (test fixtures)
│ ├── TakMeshTestRunner.kt
│ └── di/
│ └── CoreTakServerModule.kt
├── jvmAndroidMain/kotlin/ ← actual TAKServerJvm, TAKClientConnection, TakCertLoader
├── androidMain/kotlin/ ← actual AtakFileWriter (Android)
├── jvmMain/kotlin/ ← actual AtakFileWriter (Desktop), XML pull-parser
└── iosMain/kotlin/ ← actual TAKServerIos, actual compression impls
```
## Notable APIs
### `TAKServer` (interface)
```kotlin
interface TAKServer {
val connectionCount: StateFlow<Int>
var onMessage: ((CoTMessage, TAKClientInfo?) -> Unit)?
var onClientConnected: (() -> Unit)?
suspend fun start(scope: CoroutineScope): Result<Unit>
fun stop()
suspend fun broadcast(cotMessage: CoTMessage)
suspend fun broadcastRawXml(xml: String)
suspend fun hasConnections(): Boolean
}
```
The mTLS listener binds on port 8089 using a bundled `server.p12` / `ca.pem` identity, compatible with the ATAK Data Package provisioning flow.
### `TAKServerManager` (interface)
```kotlin
interface TAKServerManager {
val isRunning: StateFlow<Boolean>
val connectionCount: StateFlow<Int>
val inboundMessages: SharedFlow<InboundCoTMessage>
suspend fun start(scope: CoroutineScope)
fun stop()
suspend fun broadcast(cotMessage: CoTMessage)
suspend fun broadcastRawXml(xml: String)
}
```
`TAKServerManagerImpl` adds an **offline queue**: buffers up to 50 CoT messages for 5 minutes when no clients are connected and drains them automatically on the next `onClientConnected` callback.
### `CoTMessage`
```kotlin
@Serializable
data class CoTMessage(
val uid: String,
val type: String, // e.g. "a-f-G-U-C" (friendly ground unit)
val time: Instant,
val lat: Double, val lon: Double, val hae: Double,
val contact: CoTContact?,
val group: CoTGroup?,
val track: CoTTrack?,
val chat: CoTChat?,
val remarks: String?,
// ...
)
// Factory helpers
CoTMessage.pli(uid, callsign, lat, lon, ...) // Position Location Information
CoTMessage.chat(senderUid, callsign, message, chatroom)
```
### CoT Conversion
```kotlin
// Meshtastic proto → CoT
org.meshtastic.proto.Position.toCoTMessage(uid, callsign, team, role, battery): CoTMessage
org.meshtastic.proto.User.toCoTMessage(position, team, role, battery): CoTMessage
```
## Dependency Graph
```
core:takserver
├── api → core:repository (exported)
├── core:common, core:di, core:model, core:proto
├── okio, kotlinx.serialization.json
├── xmlutil-core, xmlutil-serialization
├── ktor-client-core, ktor-network (TCP socket)
└── zstd-jni (jvmAndroid/jvm), kotlinx.datetime
```
## Local TAK Server Feature
The Local TAK Server can be enabled from the app's Settings screen. When running, ATAK/iTAK clients on the same network can connect to `<device-ip>:8089` and their position reports are automatically bridged onto the mesh. Mesh node positions are broadcast to all connected TAK clients in real time.
## Dependency Graph
<!--region graph-->
```mermaid
graph TB
:core:takserver[takserver]:::kmp-library
:core:takserver --> :core:repository
:core:takserver -.-> :core:common
:core:takserver -.-> :core:di
:core:takserver -.-> :core:model
:core:takserver -.-> :core:proto
:core:takserver -.-> :core:testing
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef compose-desktop-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000;
classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library-compose fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;
```
<!--endregion-->
+28 -23
View File
@@ -17,29 +17,6 @@
# `:core:testing`
## Module dependency graph
<!--region graph-->
```mermaid
graph TB
:core:testing[testing]:::kmp-library
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef compose-desktop-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000;
classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library-compose fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;
```
<!--endregion-->
## Overview
The `:core:testing` module is a dedicated **Kotlin Multiplatform (KMP)** library that provides shared test fakes, doubles, rules, and utilities. It is designed to be consumed by the `commonTest` source sets of all other KMP modules to ensure consistent and unified testing behavior across the codebase.
@@ -111,3 +88,31 @@ kotlin {
}
}
```
## Dependency Graph
<!--region graph-->
```mermaid
graph TB
:core:testing[testing]:::kmp-library
:core:testing --> :core:model
:core:testing --> :core:repository
:core:testing -.-> :core:database
:core:testing -.-> :core:ble
:core:testing -.-> :core:datastore
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef compose-desktop-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000;
classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library-compose fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;
```
<!--endregion-->
+14 -1
View File
@@ -44,12 +44,25 @@ MeshtasticResourceDialog(
)
```
## Module dependency graph
## Dependency Graph
<!--region graph-->
```mermaid
graph TB
:core:ui[ui]:::kmp-library-compose
:core:ui -.-> :core:common
:core:ui -.-> :core:data
:core:ui -.-> :core:database
:core:ui -.-> :core:datastore
:core:ui -.-> :core:model
:core:ui -.-> :core:navigation
:core:ui -.-> :core:prefs
:core:ui -.-> :core:proto
:core:ui -.-> :core:repository
:core:ui -.-> :core:resources
:core:ui -.-> :core:service
:core:ui -.-> :core:testing
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
+164
View File
@@ -0,0 +1,164 @@
# `:feature:connections`
## Overview
The `:feature:connections` module provides the **device discovery and connection management screen**. It enables users to scan for and connect to Meshtastic radios over BLE, USB serial, and TCP/NSD (network service discovery). The `ScannerViewModel` is platform-neutral; Android and JVM subclasses supply platform-specific bonding and permission workflows.
**Targets:** Android · JVM (Desktop) · iOS (via `meshtastic.kmp.feature` convention plugin)
## Key Responsibilities
- Scan for BLE devices and merge results with previously bonded devices (bonded first, then discovery order)
- Enumerate USB serial devices (CH340, FTDI, CP21xx, etc.) with permission gating on Android
- Discover TCP/NSD services and manage recent manual TCP addresses
- Surface a transport-filter UI (BLE / Network / USB chips) that persists to DataStore
- Manage the full connection flow: bond → connect → disconnect
- Show connection status and progress while a device is being configured
## Source Structure
```
src/
├── commonMain/kotlin/org/meshtastic/feature/connections/
│ ├── ScannerViewModel.kt ← platform-neutral ViewModel
│ ├── Constants.kt
│ ├── model/
│ │ ├── DeviceListEntry.kt ← sealed: Ble | Usb | Tcp | Mock
│ │ └── DiscoveredDevices.kt ← GetDiscoveredDevicesUseCase interface + result
│ ├── domain/usecase/
│ │ ├── CommonGetDiscoveredDevicesUseCase.kt
│ │ ├── TcpDiscoveryHelpers.kt
│ │ └── UsbScanner.kt ← common USB scanner interface
│ ├── navigation/
│ │ └── ConnectionsNavigation.kt
│ ├── ui/
│ │ ├── ConnectionsScreen.kt
│ │ └── components/
│ │ ├── ConnectingDeviceInfo.kt
│ │ ├── ConnectionActionButton.kt
│ │ ├── CurrentlyConnectedInfo.kt
│ │ ├── DeviceList.kt / DeviceListItem.kt / DeviceSectionHeader.kt
│ │ ├── DisconnectButton.kt
│ │ ├── EmptyStateContent.kt
│ │ └── TransportFilterChips.kt
│ └── di/
│ └── FeatureConnectionsModule.kt
├── androidMain/kotlin/
│ ├── AndroidScannerViewModel.kt ← overrides bond/permission requests
│ └── domain/usecase/AndroidGetDiscoveredDevicesUseCase.kt
└── jvmMain/kotlin/
├── JvmScannerViewModel.kt
└── domain/usecase/JvmGetDiscoveredDevicesUseCase.kt
```
## Key Types
### `DeviceListEntry` (sealed class)
```kotlin
sealed class DeviceListEntry {
data class Ble(device: BleDevice, bonded: Boolean, node: Node?) : DeviceListEntry()
data class Usb(usbData: UsbDeviceData, name: String, fullAddress: String, ...) : DeviceListEntry()
data class Tcp(name: String, fullAddress: String, node: Node?) : DeviceListEntry()
data class Mock(name: String, node: Node?) : DeviceListEntry()
abstract val address: String // strips transport prefix
abstract fun copy(node: Node?): DeviceListEntry
}
```
Address format conventions: BLE → `x<MAC>`, USB → `s<path>`, TCP → `t<host:port>`, Mock → `m`.
### `ScannerViewModel`
Platform-neutral ViewModel. Exposes:
```kotlin
// Device lists
val bleDevicesForUi: StateFlow<List<DeviceListEntry>>
val usbDevicesForUi: StateFlow<List<DeviceListEntry>>
val discoveredTcpDevicesForUi: StateFlow<List<DeviceListEntry>>
val recentTcpDevicesForUi: StateFlow<List<DeviceListEntry>>
// Scan state
val isBleScanning: StateFlow<Boolean>
val isNetworkScanning: StateFlow<Boolean>
// Connection
val connectionProgressText: StateFlow<String?>
val selectedAddressFlow: StateFlow<String?>
// Actions
fun startBleScan() / stopBleScan() / toggleBleScan()
fun startNetworkScan() / stopNetworkScan()
fun onSelected(entry: DeviceListEntry): Boolean // false if bond/permission still pending
fun disconnect()
fun addRecentAddress(address: String, name: String)
fun removeRecentAddress(address: String)
```
Android and JVM subclasses override `requestBonding(entry)` and `requestPermission(entry)` to perform OS-level Bluetooth pairing and USB permission dialogs respectively.
## Navigation
```kotlin
// Registration (in androidApp / desktopApp nav graph)
fun EntryProviderScope<NavKey>.connectionsGraph(backStack: BackStack<NavKey>) {
// Registers ConnectionsRoute.Connections entry
// Injects ScannerViewModel + RadioConfigViewModel via Koin
}
```
Route: `ConnectionsRoute.Connections`
## Dependency Graph
```
feature:connections
├── core:ble, core:network (transports)
├── core:data, core:database, core:datastore, core:di
├── core:domain, core:model, core:navigation
├── core:prefs, core:proto, core:resources, core:service, core:ui
├── feature:settings (RadioConfigViewModel)
└── usb-serial-android (Android only)
```
## Dependency Graph
<!--region graph-->
```mermaid
graph TB
:feature:connections[connections]:::kmp-feature
:feature:connections -.-> :core:common
:feature:connections -.-> :core:data
:feature:connections -.-> :core:database
:feature:connections -.-> :core:datastore
:feature:connections -.-> :core:di
:feature:connections -.-> :core:domain
:feature:connections -.-> :core:model
:feature:connections -.-> :core:navigation
:feature:connections -.-> :core:prefs
:feature:connections -.-> :core:proto
:feature:connections -.-> :core:resources
:feature:connections -.-> :core:service
:feature:connections -.-> :core:ui
:feature:connections -.-> :core:ble
:feature:connections -.-> :core:network
:feature:connections -.-> :feature:settings
:feature:connections -.-> :core:testing
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef compose-desktop-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000;
classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library-compose fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;
```
<!--endregion-->
+162
View File
@@ -0,0 +1,162 @@
# `:feature:docs`
## Overview
The `:feature:docs` module is an **in-app documentation browser** with Compose Multiplatform UI. It bundles the Meshtastic user guide and developer guide as Compose resources at build time, provides full-text keyword search, Crowdin-backed multilingual content, optional ML Kit runtime translation (Google Play flavor), and a "Chirpy" AI Q&A assistant (Gemini Nano on Google Play; keyword fallback on F-Droid / Desktop / iOS).
**Targets:** Android · JVM (Desktop) · iOS (via `meshtastic.kmp.feature` convention plugin)
## Key Responsibilities
- Bundle `docs/en/user/**/*.md` and `docs/en/developer/**/*.md` as Compose resources at build time
- Sync Crowdin-translated locales into `composeResources/files/{locale}/docs/`
- Keyword search (TF-IDF style) across the full doc bundle
- Locale-aware content loading with Crowdin → ML Kit fallback chain
- Adaptive list/detail layout (single pane on phones, split pane on tablets/desktop)
- Chirpy AI assistant: streaming Q&A against in-app docs via Gemini Nano or keyword fallback
## Source Structure
```
src/commonMain/kotlin/org/meshtastic/feature/docs/
├── ai/
│ ├── AIDocAssistant.kt ← interface (Gemini Nano / keyword fallback)
│ ├── ChirpySessionHolder.kt
│ └── KeywordFallbackAssistant.kt
├── data/
│ ├── DocBundleLoader.kt ← interface + DefaultDocBundleLoader
│ └── KeywordSearchEngine.kt ← TF-IDF keyword search
├── model/
│ └── DocModels.kt ← DocSection, DocPage, DocBundle, DocSearchResult, ...
├── navigation/
│ └── DocsNavigation.kt ← docsEntries(), ChirpyUiState, rememberChirpyState()
├── translation/
│ ├── DocTranslationService.kt ← ML Kit (Google) or no-op (fdroid/desktop/iOS)
│ ├── DocTranslationCache.kt
│ ├── MarkdownTranslationSegmenter.kt
│ └── NoOpDocTranslator.kt
└── ui/
├── DocsBrowserScreen.kt ← list pane
├── DocsPageRouteScreen.kt ← detail pane
├── DocsSearchBar.kt
├── ChirpyAssistantSheet.kt ← AI assistant bottom sheet
├── ComposeResourceImageTransformer.kt
├── DocPageIconResolver.kt
└── DocsPreviews.kt
```
## Key Types
### `DocSection` (sealed interface)
```kotlin
sealed interface DocSection {
data object UserGuide : DocSection
data object DeveloperGuide : DocSection
}
```
### `DocPage`
```kotlin
data class DocPage(
val id: String,
val title: String,
val section: DocSection,
val navOrder: Int,
val resourcePath: String,
val keywords: List<String>,
val charCount: Int,
)
```
### `AIDocAssistant` (interface)
```kotlin
interface AIDocAssistant {
suspend fun isSupported(): Boolean
suspend fun answer(question: String, currentPageId: String? = null): AIDocAssistantResult
fun answerStream(question: String, currentPageId: String? = null): Flow<AIDocAssistantResult>
fun resetSession()
}
```
`AIDocAssistantResult` is a sealed interface: `Partial`, `Success`, `Fallback`, `Error`.
Platform bindings:
- **Google flavor**: Gemini Nano via on-device ML
- **F-Droid / Desktop / iOS**: `KeywordFallbackAssistant` (keyword search + summarisation, no network)
### `ChirpyMessage`
```kotlin
@Serializable
data class ChirpyMessage(
val id: String,
val role: ChirpyRole, // USER | ASSISTANT | SYSTEM
val text: String,
val sources: List<SourceRef>,
)
```
## Gradle Tasks
Two custom tasks keep bundled docs in sync:
| Task | Description |
|---|---|
| `syncDocsToComposeResources` | Copies `docs/en/user/**/*.md`, `docs/en/developer/**/*.md`, and `docs/screenshots/**/*.png` into `src/commonMain/composeResources/files/docs/`. Runs automatically before resource generation. |
| `syncTranslatedDocsToComposeResources` | Copies Crowdin-translated locales from `docs/{locale}/user/**/*.md` into `src/commonMain/composeResources/files/{locale}/docs/` using CMP qualifier format (e.g. `pt-rBR`). |
These tasks run automatically — no manual invocation is required during normal development.
## Navigation
Routes (registered under the Settings nav graph):
| Route | Description |
|---|---|
| `SettingsRoute.HelpDocs` | Doc browser list pane |
| `SettingsRoute.HelpDocPage` | Individual doc page detail pane |
The `docsEntries()` extension uses Material3 adaptive `ListDetailSceneStrategy` to automatically provide a split-pane layout on large screens.
## Dependency Graph
```
feature:docs
├── core:common, core:navigation, core:resources, core:ui, core:di
├── coil (image loading in Markdown)
├── markdown-renderer-m3 (Compose Markdown rendering)
├── compose.material3.adaptive, compose.material3.adaptive.navigation3
└── kotlinx.collections.immutable
```
## Dependency Graph
<!--region graph-->
```mermaid
graph TB
:feature:docs[docs]:::kmp-feature
:feature:docs -.-> :core:common
:feature:docs -.-> :core:navigation
:feature:docs -.-> :core:resources
:feature:docs -.-> :core:ui
:feature:docs -.-> :core:di
:feature:docs -.-> :core:testing
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef compose-desktop-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000;
classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library-compose fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;
```
<!--endregion-->
+38 -23
View File
@@ -1,28 +1,5 @@
# `:feature:firmware`
## Module dependency graph
<!--region graph-->
```mermaid
graph TB
:feature:firmware[firmware]:::kmp-feature
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef compose-desktop-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000;
classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library-compose fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;
```
<!--endregion-->
## Firmware Update System
The `:feature:firmware` module provides a unified interface for updating Meshtastic devices across different platforms and connection types.
@@ -113,3 +90,41 @@ sequenceDiagram
- `SecureDfuTransport.kt`: BLE transport layer for Secure DFU using Kable (control/data point characteristics, PRN flow control).
- `DfuZipParser.kt`: Parses Nordic DFU ZIP archives (manifest, init packet, firmware binary).
- `UsbUpdateHandler.kt`: Handles USB/UF2 firmware updates across platforms.
## Dependency Graph
<!--region graph-->
```mermaid
graph TB
:feature:firmware[firmware]:::kmp-feature
:feature:firmware -.-> :core:ble
:feature:firmware -.-> :core:common
:feature:firmware -.-> :core:data
:feature:firmware -.-> :core:database
:feature:firmware -.-> :core:datastore
:feature:firmware -.-> :core:di
:feature:firmware -.-> :core:model
:feature:firmware -.-> :core:navigation
:feature:firmware -.-> :core:network
:feature:firmware -.-> :core:prefs
:feature:firmware -.-> :core:proto
:feature:firmware -.-> :core:service
:feature:firmware -.-> :core:resources
:feature:firmware -.-> :core:ui
:feature:firmware -.-> :core:testing
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef compose-desktop-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000;
classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library-compose fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;
```
<!--endregion-->
+12 -1
View File
@@ -1,6 +1,9 @@
# `:feature:intro`
## Overview
**Targets:** Android · JVM (Desktop) · iOS
The `:feature:intro` module provides the onboarding experience for new users. It handles the initial welcome flow and requests mandatory permissions (Location, Bluetooth, Notifications).
## Key Components
@@ -14,12 +17,20 @@ Dedicated screens for explaining and requesting specific permissions:
- `BluetoothScreen`: Necessary for connecting to radios.
- `NotificationsScreen`: Necessary for foreground service and message alerts.
## Module dependency graph
## Dependency Graph
<!--region graph-->
```mermaid
graph TB
:feature:intro[intro]:::kmp-feature
:feature:intro -.-> :core:service
:feature:intro -.-> :core:common
:feature:intro -.-> :core:model
:feature:intro -.-> :core:repository
:feature:intro -.-> :core:ui
:feature:intro -.-> :core:resources
:feature:intro -.-> :core:testing
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
+14 -1
View File
@@ -38,12 +38,25 @@ All providers are injected via `CompositionLocal` in `MainActivity.kt` and consu
- **Traceroute Visualization**: Dedicated map view showing route segments between mesh nodes.
- **Offline Maps**: Support for pre-downloaded map tiles (via `osmdroid`).
## Module dependency graph
## Dependency Graph
<!--region graph-->
```mermaid
graph TB
:feature:map[map]:::kmp-feature
:feature:map -.-> :core:data
:feature:map -.-> :core:database
:feature:map -.-> :core:datastore
:feature:map -.-> :core:model
:feature:map -.-> :core:navigation
:feature:map -.-> :core:prefs
:feature:map -.-> :core:proto
:feature:map -.-> :core:service
:feature:map -.-> :core:resources
:feature:map -.-> :core:ui
:feature:map -.-> :core:di
:feature:map -.-> :core:testing
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
+14 -1
View File
@@ -20,12 +20,25 @@ A security-focused utility that detects and transforms homoglyphs (visually simi
- **Message Reactions**: Support for reacting to messages with emojis.
- **Delivery Status**: Indicators for "Sent", "Received", and "Read" (ACK/NACK).
## Module dependency graph
## Dependency Graph
<!--region graph-->
```mermaid
graph TB
:feature:messaging[messaging]:::kmp-feature
:feature:messaging -.-> :core:common
:feature:messaging -.-> :core:data
:feature:messaging -.-> :core:database
:feature:messaging -.-> :core:domain
:feature:messaging -.-> :core:model
:feature:messaging -.-> :core:navigation
:feature:messaging -.-> :core:prefs
:feature:messaging -.-> :core:proto
:feature:messaging -.-> :core:resources
:feature:messaging -.-> :core:service
:feature:messaging -.-> :core:ui
:feature:messaging -.-> :core:testing
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
+20 -1
View File
@@ -1,6 +1,9 @@
# `:feature:node`
## Overview
**Targets:** Android · JVM (Desktop) · iOS
The `:feature:node` module handles node-centric features, including the node list, detailed node information, telemetry charts, and the compass.
## Key Components
@@ -17,12 +20,28 @@ Manages the retrieval and display of telemetry data (e.g., battery, SNR, environ
### 4. `CompassViewModel`
Provides a compass interface to show the relative direction and distance to other nodes.
## Module dependency graph
## Dependency Graph
<!--region graph-->
```mermaid
graph TB
:feature:node[node]:::kmp-feature
:feature:node -.-> :core:common
:feature:node -.-> :core:data
:feature:node -.-> :core:database
:feature:node -.-> :core:datastore
:feature:node -.-> :core:domain
:feature:node -.-> :core:model
:feature:node -.-> :core:navigation
:feature:node -.-> :core:proto
:feature:node -.-> :core:repository
:feature:node -.-> :core:resources
:feature:node -.-> :core:service
:feature:node -.-> :core:ui
:feature:node -.-> :core:di
:feature:node -.-> :feature:map
:feature:node -.-> :core:testing
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
+20 -1
View File
@@ -19,12 +19,31 @@ Displays version information, licenses, and project links.
- **Node Database Management**: Options to clear or prune the local and remote node databases.
- **App Preferences**: Theme selection, unit system (metric/imperial), and notification settings.
## Module dependency graph
## Dependency Graph
<!--region graph-->
```mermaid
graph TB
:feature:settings[settings]:::kmp-feature
:feature:settings -.-> :core:barcode
:feature:settings -.-> :core:nfc
:feature:settings -.-> :core:common
:feature:settings -.-> :core:data
:feature:settings -.-> :core:database
:feature:settings -.-> :core:datastore
:feature:settings -.-> :core:domain
:feature:settings -.-> :core:model
:feature:settings -.-> :core:navigation
:feature:settings -.-> :core:network
:feature:settings -.-> :core:proto
:feature:settings -.-> :core:repository
:feature:settings -.-> :core:service
:feature:settings -.-> :core:resources
:feature:settings -.-> :core:ui
:feature:settings -.-> :core:di
:feature:settings -.-> :core:takserver
:feature:settings -.-> :core:testing
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
+149
View File
@@ -0,0 +1,149 @@
# `:feature:widget`
## Overview
The `:feature:widget` module is an **Android-only Jetpack Glance home-screen widget** (API 26+). It exposes a single widget — `LocalStatsWidget` — that displays live mesh statistics sourced from the running Meshtastic service.
**Target: Android only** (not KMP — uses `meshtastic.android.library` + `meshtastic.android.library.compose` convention plugins)
## Key Responsibilities
- Display live mesh statistics on the Android home screen via Jetpack Glance
- Combine four reactive data sources into a single `StateFlow<LocalStatsWidgetUiState>` for efficient re-renders
- Support three responsive widget sizes: small square, wide rectangle, and large square
- Fulfil the `AppWidgetUpdater` interface from `:core:repository` so the mesh service can trigger widget refreshes without depending on Android widget APIs
## Source Structure
```
src/main/kotlin/org/meshtastic/feature/widget/
├── LocalStatsWidget.kt ← GlanceAppWidget, 3 responsive sizes
├── LocalStatsWidgetReceiver.kt ← GlanceAppWidgetReceiver
├── LocalStatsWidgetState.kt ← LocalStatsWidgetUiState + StateProvider
├── AndroidAppWidgetUpdater.kt ← implements AppWidgetUpdater (core:repository)
├── RefreshLocalStatsAction.kt ← GlanceActionCallback (refresh button)
└── di/
└── FeatureWidgetModule.kt
```
## Widget Content
The widget shows the following information when connected:
| Section | Data shown |
|---|---|
| Node identity | Short name chip with node colours |
| Battery | Level percentage + progress bar |
| Channel utilization | Percentage + progress bar |
| Air utilization | Percentage + progress bar |
| Traffic | TX / RX packet counts, duplicates, relay TX, relay cancelled, dropped, bad RX |
| Diagnostics | Noise floor (dBm), free heap / total heap |
| Footer | Online nodes / total nodes, device uptime, last-updated timestamp |
When disconnected, the widget shows a "Not Connected" state with the last-known node name.
## Key Types
### `LocalStatsWidgetUiState`
```kotlin
data class LocalStatsWidgetUiState(
val connectionState: ConnectionState,
val isConnecting: Boolean,
val showContent: Boolean,
// Node identity
val nodeShortName: String?,
val nodeColors: Pair<Int, Int>?,
// Battery
val batteryLevel: Int?,
val hasBattery: Boolean,
// Utilization
val channelUtilization: Float,
val airUtilization: Float,
// Traffic counters
val numPacketsTx: Int, val numPacketsRx: Int,
val numRxDupe: Int, val numTxRelay: Int, val numTxRelayCanceled: Int,
val numTxDropped: Int, val numPacketsRxBad: Int,
// Diagnostics
val noiseFloor: Int?,
val heapFreeBytes: Long, val heapTotalBytes: Long,
// Footer
val totalNodes: Int, val onlineNodes: Int,
val uptimeSecs: Int?,
val updateTimeMillis: Long,
)
```
### `LocalStatsWidgetStateProvider`
Koin `@Single` that eagerly combines four repository flows:
- `ServiceRepository.connectionState`
- `NodeRepository.nodeDBbyNum` (online/total counts)
- `NodeRepository.localStats`
- `NodeRepository.ourNodeInfo`
```kotlin
val state: StateFlow<LocalStatsWidgetUiState>
```
### `LocalStatsWidget`
```kotlin
class LocalStatsWidget : GlanceAppWidget(), KoinComponent {
override val sizeMode = SizeMode.Responsive(
setOf(DpSize(100.dp, 100.dp), DpSize(250.dp, 100.dp), DpSize(250.dp, 250.dp))
)
}
```
Tapping the widget opens the app. The refresh button fires `RefreshLocalStatsAction`, which calls `AppWidgetUpdater.updateAll()`.
## `AppWidgetUpdater` integration
The mesh service holds a reference to `AppWidgetUpdater` (defined in `:core:repository`) without depending on Android widget APIs. `AndroidAppWidgetUpdater` in this module provides the concrete implementation:
```kotlin
class AndroidAppWidgetUpdater : AppWidgetUpdater {
override suspend fun updateAll() {
LocalStatsWidget().updateAll(context)
}
}
```
This keeps the service layer platform-agnostic while still enabling widget refresh on data changes.
## Dependency Graph
```
feature:widget (Android only)
├── core:common, core:model, core:resources, core:repository
├── androidx.glance.appwidget, glance.material3, glance.preview
└── compose.multiplatform.ui (LocalConfiguration, LocalDensity)
```
## Dependency Graph
<!--region graph-->
```mermaid
graph TB
:feature:widget[widget]:::android-feature
:feature:widget -.-> :core:common
:feature:widget -.-> :core:model
:feature:widget -.-> :core:resources
:feature:widget -.-> :core:repository
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef compose-desktop-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000;
classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library-compose fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;
```
<!--endregion-->
+30 -23
View File
@@ -1,28 +1,5 @@
# `:feature:wifi-provision`
## Module dependency graph
<!--region graph-->
```mermaid
graph TB
:feature:wifi-provision[wifi-provision]:::kmp-feature
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef compose-desktop-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000;
classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library-compose fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;
```
<!--endregion-->
## WiFi Provisioning System — for mPWRD-OS
The `:feature:wifi-provision` module provides BLE-based WiFi provisioning for [mPWRD-OS](https://github.com/mPWRD-OS/mPWRD-OS) devices using the Nymea network manager protocol. mPWRD-OS is a community project that combines Armbian and Meshtastic for Linux-native mesh networking hardware. This module scans for provisioning-capable devices, retrieves available WiFi networks, and applies credentials — all over BLE via the Kable multiplatform library.
@@ -65,3 +42,33 @@ sequenceDiagram
- `NymeaPacketCodec.kt`: Chunked BLE packet encoder/decoder with reassembly.
- `NymeaProtocol.kt`: JSON serialization for Nymea network manager commands and responses.
- `ProvisionStatusCard.kt`: Inline status feedback card (idle/success/failed) with Material 3 colors.
## Dependency Graph
<!--region graph-->
```mermaid
graph TB
:feature:wifi-provision[wifi-provision]:::kmp-feature
:feature:wifi-provision -.-> :core:ble
:feature:wifi-provision -.-> :core:common
:feature:wifi-provision -.-> :core:di
:feature:wifi-provision -.-> :core:navigation
:feature:wifi-provision -.-> :core:resources
:feature:wifi-provision -.-> :core:ui
:feature:wifi-provision -.-> :core:testing
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef compose-desktop-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000;
classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library-compose fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000;
classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000;
```
<!--endregion-->