mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-06-01 22:19:18 +02:00
docs: comprehensive documentation audit and refresh (#5572)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+3
-1
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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,172 +113,151 @@ 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 ->
|
||||
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++
|
||||
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()
|
||||
}
|
||||
|
||||
// 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" }
|
||||
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 }
|
||||
?.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++
|
||||
}
|
||||
?.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 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,
|
||||
)
|
||||
}
|
||||
|
||||
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> {
|
||||
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 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 {
|
||||
inAliases = false
|
||||
result["aliases_raw"] = aliasesBuilder.toString().trimEnd(',')
|
||||
}
|
||||
}
|
||||
if (!inAliases && line.contains(":")) {
|
||||
val (key, value) = line.split(":", limit = 2)
|
||||
result[key.trim()] = value.trim()
|
||||
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()
|
||||
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 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)
|
||||
}
|
||||
|
||||
!inAliases && line.contains(":") -> result += parseLine(line)
|
||||
}
|
||||
if (inAliases) result["aliases_raw"] = aliasesBuilder.toString().trimEnd(',')
|
||||
return result
|
||||
}
|
||||
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> {
|
||||
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
|
||||
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)
|
||||
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>()
|
||||
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 >= 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("&", "&").replace("<", "<").replace(">", ">")
|
||||
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 """
|
||||
.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
val dir = if (locale == "ar") "rtl" else "ltr"
|
||||
val cssPath = if (locale != "en") "../../styles/docs.css" else "../styles/docs.css"
|
||||
return """
|
||||
|<!DOCTYPE html>
|
||||
|<html lang="$locale" dir="$dir">
|
||||
|<head>
|
||||
@@ -275,136 +270,135 @@ abstract class GenerateDocsBundleTask : DefaultTask() {
|
||||
|<pre class="markdown-content">$content</pre>
|
||||
|</body>
|
||||
|</html>
|
||||
""".trimMargin()
|
||||
}
|
||||
|
||||
private fun generateCss(): String = """
|
||||
|:root {
|
||||
| --primary: #2C2D3C;
|
||||
| --accent: #67EA94;
|
||||
| --accent-text: #3FB86D;
|
||||
| --info: #5C6BC0;
|
||||
| --warning: #E8A33E;
|
||||
| --error: #E05252;
|
||||
|}
|
||||
|body {
|
||||
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
| line-height: 1.6;
|
||||
| padding: 16px;
|
||||
| color: var(--primary);
|
||||
| max-width: 800px;
|
||||
| margin: 0 auto;
|
||||
|}
|
||||
|@media (prefers-color-scheme: dark) {
|
||||
| body { background: #1A1B26; color: #ECEDF3; }
|
||||
| pre { background: #2C2D3C; }
|
||||
|}
|
||||
|pre.markdown-content {
|
||||
| white-space: pre-wrap;
|
||||
| word-wrap: break-word;
|
||||
| font-family: inherit;
|
||||
| background: transparent;
|
||||
| padding: 0;
|
||||
| margin: 0;
|
||||
|}
|
||||
|.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()
|
||||
}
|
||||
|
||||
private fun generateCss(): String =
|
||||
"""
|
||||
|:root {
|
||||
| --primary: #2C2D3C;
|
||||
| --accent: #67EA94;
|
||||
| --accent-text: #3FB86D;
|
||||
| --info: #5C6BC0;
|
||||
| --warning: #E8A33E;
|
||||
| --error: #E05252;
|
||||
|}
|
||||
|body {
|
||||
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
| line-height: 1.6;
|
||||
| padding: 16px;
|
||||
| color: var(--primary);
|
||||
| max-width: 800px;
|
||||
| margin: 0 auto;
|
||||
|}
|
||||
|@media (prefers-color-scheme: dark) {
|
||||
| body { background: #1A1B26; color: #ECEDF3; }
|
||||
| pre { background: #2C2D3C; }
|
||||
|}
|
||||
|pre.markdown-content {
|
||||
| white-space: pre-wrap;
|
||||
| word-wrap: break-word;
|
||||
| font-family: inherit;
|
||||
| background: transparent;
|
||||
| padding: 0;
|
||||
| margin: 0;
|
||||
|}
|
||||
|.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()
|
||||
|
||||
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()) {
|
||||
"release" -> "v${version.get()}"
|
||||
"root" -> ""
|
||||
else -> channel.get()
|
||||
}
|
||||
val channelPath =
|
||||
when (channel.get()) {
|
||||
"release" -> "v${version.get()}"
|
||||
"root" -> ""
|
||||
else -> channel.get()
|
||||
}
|
||||
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
@@ -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-->
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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-->
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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-->
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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-->
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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-->
|
||||
@@ -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-->
|
||||
|
||||
Reference in New Issue
Block a user