Compare commits

..

No commits in common. "main" and "v2.7.14-internal.56" have entirely different histories.

189 changed files with 938 additions and 3624 deletions

12
.github/lsp.json vendored
View file

@ -1,12 +0,0 @@
{
"lspServers": {
"kotlin": {
"command": "kotlin-language-server",
"args": [],
"fileExtensions": {
".kt": "kotlin",
".kts": "kotlin"
}
}
}
}

View file

@ -56,15 +56,6 @@
"changelogUrl": "https://github.com/meshtastic/protobufs/compare/{{currentDigest}}...{{newDigest}}",
"automerge": true
},
{
"description": "Group CMP and the androidx.compose artifacts that track it so Renovate bumps them together (see PR #5180)",
"groupName": "compose-multiplatform",
"matchPackageNames": [
"/^org\\.jetbrains\\.compose/",
"androidx.compose.runtime:runtime-tracing",
"androidx.compose.ui:ui-test-manifest"
]
},
{
"description": "Restrict sensitive infrastructure to manual minor updates",
"matchUpdateTypes": [

View file

@ -44,16 +44,13 @@ jobs:
uses: actions/ai-inference@v2
id: quality
continue-on-error: true
env:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
with:
max-tokens: 20
prompt: |
Is this GitHub pull request spam, AI-generated slop, or low quality?
Title: ${{ env.PR_TITLE }}
Body: ${{ env.PR_BODY }}
Title: ${{ github.event.pull_request.title }}
Body: ${{ github.event.pull_request.body }}
Respond with exactly one of: spam, ai-generated, needs-review, ok
system-prompt: You detect spam and low-quality contributions. Be conservative - only flag obvious spam or AI slop.
@ -97,9 +94,6 @@ jobs:
uses: actions/ai-inference@v2
id: classify
continue-on-error: true
env:
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
with:
max-tokens: 30
prompt: |
@ -111,8 +105,8 @@ jobs:
Use enhancement if it adds a new feature, improves performance, or adds new functionality.
Use refactor if it restructures code without changing behavior, cleans up code, or improves architecture.
Title: ${{ env.PR_TITLE }}
Body: ${{ env.PR_BODY }}
Title: ${{ github.event.pull_request.title }}
Body: ${{ github.event.pull_request.body }}
system-prompt: You classify pull requests into categories. Be conservative and pick the most appropriate single label.
model: openai/gpt-4o-mini

1
.gitignore vendored
View file

@ -55,4 +55,3 @@ wireless-install.sh
firebase-debug.log
.agent_plans/
.agent_refs/
.agent_artifacts/

View file

@ -1,295 +0,0 @@
diff --git a/core/common/src/commonMain/kotlin/org/meshtastic/core/common/di/ApplicationCoroutineScope.kt b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/di/ApplicationCoroutineScope.kt
new file mode 100644
index 0000000000..2a27b96906
--- /dev/null
+++ b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/di/ApplicationCoroutineScope.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.meshtastic.core.common.di
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import org.koin.core.annotation.Single
+import org.meshtastic.core.common.util.ioDispatcher
+
+/**
+ * A process-wide [CoroutineScope] that outlives individual ViewModels and UI components.
+ *
+ * Use this scope for fire-and-forget cleanup work that must continue after a ViewModel's own scope has been cancelled
+ * (for example, deleting temporary files in `onCleared()`). Backed by a [SupervisorJob] so failures in one child do not
+ * cancel siblings, and by [ioDispatcher] so work runs off the main thread.
+ *
+ * Prefer scoping work to a more specific scope (like `viewModelScope`) whenever possible; this scope is an escape hatch
+ * and should be used sparingly.
+ */
+interface ApplicationCoroutineScope : CoroutineScope
+
+@Single(binds = [ApplicationCoroutineScope::class])
+internal class ApplicationCoroutineScopeImpl : ApplicationCoroutineScope {
+ override val coroutineContext = SupervisorJob() + ioDispatcher
+}
diff --git a/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt b/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt
index 231c84d401..5365ab95e2 100644
--- a/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt
+++ b/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt
@@ -37,12 +37,12 @@ import androidx.lifecycle.compose.LifecycleEventEffect
import co.touchlab.kermit.Logger
import com.eygraber.uri.toAndroidUri
import com.eygraber.uri.toKmpUri
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.getString
import org.meshtastic.core.common.gpsDisabled
import org.meshtastic.core.common.util.CommonUri
+import org.meshtastic.core.common.util.ioDispatcher
import java.net.URLEncoder
@Composable
@@ -146,7 +146,7 @@ actual fun rememberReadTextFromUri(): suspend (uri: CommonUri, maxChars: Int) ->
val context = LocalContext.current
return remember(context) {
{ uri, maxChars ->
- withContext(Dispatchers.IO) {
+ withContext(ioDispatcher) {
@Suppress("TooGenericExceptionCaught")
try {
val androidUri = uri.toAndroidUri()
diff --git a/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt b/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt
index 031e1fe35d..a938f92ea6 100644
--- a/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt
+++ b/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt
@@ -20,10 +20,10 @@ package org.meshtastic.core.ui.util
import androidx.compose.runtime.Composable
import co.touchlab.kermit.Logger
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.compose.resources.StringResource
import org.meshtastic.core.common.util.CommonUri
+import org.meshtastic.core.common.util.ioDispatcher
import java.awt.Desktop
import java.awt.FileDialog
import java.awt.Frame
@@ -89,7 +89,7 @@ actual fun rememberOpenFileLauncher(onUriReceived: (CommonUri?) -> Unit): (mimeT
/** JVM — Reads text from a file URI. */
@Composable
actual fun rememberReadTextFromUri(): suspend (uri: CommonUri, maxChars: Int) -> String? = { uri, maxChars ->
- withContext(Dispatchers.IO) {
+ withContext(ioDispatcher) {
@Suppress("TooGenericExceptionCaught")
try {
val file = File(URI(uri.toString()))
diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt
index dc1c459716..f8ff9fcac8 100644
--- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt
+++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt
@@ -35,6 +35,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import org.jetbrains.compose.resources.StringResource
import org.koin.core.annotation.KoinViewModel
+import org.meshtastic.core.common.di.ApplicationCoroutineScope
import org.meshtastic.core.common.util.CommonUri
import org.meshtastic.core.common.util.safeCatching
import org.meshtastic.core.database.entity.FirmwareRelease
@@ -91,6 +92,7 @@ class FirmwareUpdateViewModel(
private val firmwareUpdateManager: FirmwareUpdateManager,
private val usbManager: FirmwareUsbManager,
private val fileHandler: FirmwareFileHandler,
+ private val applicationScope: ApplicationCoroutineScope,
) : ViewModel() {
private val _state = MutableStateFlow<FirmwareUpdateState>(FirmwareUpdateState.Idle)
@@ -124,12 +126,10 @@ class FirmwareUpdateViewModel(
override fun onCleared() {
super.onCleared()
- // viewModelScope is already cancelled when onCleared() runs, so launch cleanup in a
- // standalone scope. SupervisorJob prevents the coroutine from propagating failures to a
- // shared parent, and NonCancellable on the launch keeps cleanup running even if the scope
- // is cancelled concurrently.
- @OptIn(kotlinx.coroutines.DelicateCoroutinesApi::class)
- kotlinx.coroutines.GlobalScope.launch(NonCancellable) {
+ // viewModelScope is already cancelled when onCleared() runs, so launch cleanup on the
+ // application-wide scope (SupervisorJob + ioDispatcher). NonCancellable keeps cleanup
+ // running even if something tries to cancel it mid-flight.
+ applicationScope.launch(NonCancellable) {
tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile)
}
}
diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt
index 4c48a1ced5..030d84effd 100644
--- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt
+++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt
@@ -108,6 +108,7 @@ class FirmwareUpdateIntegrationTest {
firmwareUpdateManager,
usbManager,
fileHandler,
+ TestApplicationCoroutineScope(testDispatcher),
)
@Test
diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt
index 7032ed4088..a8eddff838 100644
--- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt
+++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt
@@ -124,6 +124,7 @@ class FirmwareUpdateViewModelTest {
firmwareUpdateManager,
usbManager,
fileHandler,
+ TestApplicationCoroutineScope(testDispatcher),
)
@Test
diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/TestApplicationCoroutineScope.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/TestApplicationCoroutineScope.kt
new file mode 100644
index 0000000000..3ef5c44ef4
--- /dev/null
+++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/TestApplicationCoroutineScope.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package org.meshtastic.feature.firmware
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import org.meshtastic.core.common.di.ApplicationCoroutineScope
+
+internal class TestApplicationCoroutineScope(dispatcher: CoroutineDispatcher) :
+ ApplicationCoroutineScope,
+ CoroutineScope by CoroutineScope(SupervisorJob() + dispatcher)
diff --git a/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt b/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt
index acb1545bdd..23a0d03ab2 100644
--- a/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt
+++ b/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt
@@ -116,6 +116,7 @@ class FirmwareUpdateViewModelFileTest {
firmwareUpdateManager,
usbManager,
fileHandler,
+ TestApplicationCoroutineScope(testDispatcher),
)
// -----------------------------------------------------------------------
diff --git a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt
index c251b4d5ef..315ad1da85 100644
--- a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt
@@ -27,6 +27,7 @@ import co.touchlab.kermit.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import org.meshtastic.core.common.util.ioDispatcher
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.debug_export_failed
import org.meshtastic.core.resources.debug_export_success
@@ -48,7 +49,7 @@ actual fun rememberLogExporter(logsProvider: suspend () -> List<DebugViewModel.U
}
private suspend fun exportAllLogsToUri(context: Context, targetUri: Uri, logs: List<DebugViewModel.UiMeshLog>) =
- withContext(Dispatchers.IO) {
+ withContext(ioDispatcher) {
try {
if (logs.isEmpty()) {
withContext(Dispatchers.Main) { context.showToast(Res.string.debug_export_failed, "No logs to export") }
diff --git a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt
index 9afde85e5f..a28a576788 100644
--- a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt
@@ -24,9 +24,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import co.touchlab.kermit.Logger
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import org.meshtastic.core.common.util.ioDispatcher
@Composable
actual fun rememberDataPackageExporter(dataPackageProvider: suspend () -> ByteArray): (fileName: String) -> Unit {
@@ -41,7 +41,7 @@ actual fun rememberDataPackageExporter(dataPackageProvider: suspend () -> ByteAr
return { fileName -> exportLauncher.launch(fileName) }
}
-private suspend fun exportZipToUri(context: Context, targetUri: Uri, data: ByteArray) = withContext(Dispatchers.IO) {
+private suspend fun exportZipToUri(context: Context, targetUri: Uri, data: ByteArray) = withContext(ioDispatcher) {
try {
context.contentResolver.openOutputStream(targetUri)?.use { os -> os.write(data) }
Logger.i { "TAK data package exported successfully to $targetUri" }
diff --git a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt
index 5b63cc90a3..a9a7285593 100644
--- a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt
+++ b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt
@@ -19,9 +19,9 @@ package org.meshtastic.feature.settings.debugging
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import co.touchlab.kermit.Logger
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import org.meshtastic.core.common.util.ioDispatcher
import java.awt.FileDialog
import java.awt.Frame
import java.io.File
@@ -41,7 +41,7 @@ actual fun rememberLogExporter(logsProvider: suspend () -> List<DebugViewModel.U
return@launch
}
- withContext(Dispatchers.IO) {
+ withContext(ioDispatcher) {
// Run file dialog to ask user where to save
val fileDialog = FileDialog(null as Frame?, "Export Logs", FileDialog.SAVE)
fileDialog.file = fileName
diff --git a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt
index 9fb71379fc..bfbb85bc0d 100644
--- a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt
+++ b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt
@@ -19,9 +19,9 @@ package org.meshtastic.feature.settings.tak
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import co.touchlab.kermit.Logger
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import org.meshtastic.core.common.util.ioDispatcher
import java.awt.FileDialog
import java.awt.Frame
import java.io.File
@@ -44,7 +44,7 @@ actual fun rememberDataPackageExporter(dataPackageProvider: suspend () -> ByteAr
if (directory != null && file != null) {
val targetFile = File(directory, file)
val data = dataPackageProvider()
- withContext(Dispatchers.IO) { targetFile.writeBytes(data) }
+ withContext(ioDispatcher) { targetFile.writeBytes(data) }
Logger.i { "TAK data package exported successfully to ${targetFile.absolutePath}" }
}
}

View file

@ -12,26 +12,15 @@ This skill covers dependency injection (Koin Annotations 4.2.x) and JetBrains Na
4. **Resolution:** Resolve app-layer wrappers via `koinViewModel()` or injected bindings within Compose navigation graphs.
### Anti-Patterns
- **A1 Module Compile Safety:** Do **not** enable `compileSafety`. It is a single boolean that enables A1 per-module checks — there is no separate A3 full-graph mode. Runtime graph verification is handled by `KoinVerificationTest` and `DesktopKoinTest` instead.
- **A1 Module Compile Safety:** Do **not** enable A1 `compileSafety`. We rely on Koin's A3 full-graph validation (`startKoin` / `VerifyModule`) because of our decoupled Clean Architecture design (interfaces in one module, implemented in another).
- **Default Parameters:** Do **not** expect Koin to inject default parameters automatically. The K2 plugin's `skipDefaultValues = true` behavior skips parameters with default Kotlin values.
### Koin Startup Pattern (K2 Compiler Plugin)
The project uses the **K2 Compiler Plugin** (`koin-compiler-plugin`, not KSP). The canonical startup uses the plugin's typed `startKoin<T>()` stub, which the plugin transforms at compile time via IR:
The project uses the **K2 Compiler Plugin** (`koin-compiler-plugin`, not KSP). The correct canonical startup for this path is:
```kotlin
// Bootstrap class — separate from @Module, references the root module graph
@KoinApplication(modules = [AppKoinModule::class])
object AndroidKoinApp
// In Application.onCreate()
startKoin<AndroidKoinApp> {
androidContext(this@MeshUtilApplication)
workManagerFactory()
}
startKoin { modules(AppKoinModule().module()) }
```
- `@KoinApplication` goes on a **dedicated bootstrap object**, not on a `@Module` class.
- `startKoin<T>()` (from `org.koin.plugin.module.dsl`) is a compiler plugin stub — if the plugin isn't applied, it throws `NotImplementedError`.
- `stopKoin()` uses the standard runtime API (`org.koin.core.context.stopKoin`).
- `compileSafety` must stay **disabled** — it enables A1 per-module checks that break our inverted-dependency architecture. There is no separate A3 full-graph flag.
Do **not** use `@KoinApplication` — that annotation is part of the **KSP annotations path** (`koin-ksp-compiler`) and generates a `startKoin()` extension via KSP. It is incompatible with the K2 plugin approach. The two paths are mutually exclusive; the project has deliberately chosen K2 for compile-time wiring without KSP overhead.
## Navigation 3
@ -50,7 +39,6 @@ startKoin<AndroidKoinApp> {
## Reference Anchors
- **App Startup / Koin Bootstrap:** `app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt`
- **DI Bootstrap Object:** `app/src/main/kotlin/org/meshtastic/app/di/AndroidKoinApp.kt`
- **DI App Wiring:** `app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt`
- **Shared Routes:** `core/navigation/src/commonMain/kotlin/org/meshtastic/core/navigation/Routes.kt`
- **Desktop Nav Shell:** `desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt`

View file

@ -1,79 +0,0 @@
# Skill: New Branch Bootstrap
## Description
Canonical recipe for spinning up a fresh working branch off the latest upstream `main`. Use this skill
whenever the user says things like *"make a new branch off fetched origin/main"*, *"peel off a fresh
branch"*, *"dust off #NNNN"*, or otherwise signals the start of a new unit of work.
This replaces the ad-hoc prose that used to be retyped at the start of every session.
## When to Use
- Starting any new feature, fix, chore, or refactor.
- Rebasing a stale PR onto current `main` (see [Rebase variant](#rebase-variant)).
- Reproducing a CI failure from a clean baseline.
## Preconditions (verify before branching)
1. **Clean worktree.** If `git status --porcelain` is non-empty, ask the user before proceeding.
2. **Upstream remote present.** `git remote -v` must list `upstream` pointing at
`meshtastic/Meshtastic-Android`. If only `origin` exists on a fork, treat `origin` as upstream.
3. **Submodules initialised.** `core/proto/src/main/proto` must be populated — see AGENTS.md
workspace bootstrap rules.
4. **Secrets bootstrapped.** If `local.properties` is missing, copy `secrets.defaults.properties`
(required for `google` flavor builds).
## Standard Recipe
```bash
# 1. Fetch latest upstream
git fetch upstream --prune --tags
# 2. Create the branch from upstream/main (never from a local stale main)
git switch -c <branch-name> upstream/main
# 3. Ensure submodules track the new base
git submodule update --init --recursive
# 4. Sanity check
git --no-pager log -1 --oneline
```
## Branch Naming
Use conventional-commit style prefixes that match the PR title convention in AGENTS.md
`<git_and_prs>`:
| Prefix | Use for |
| :--- | :--- |
| `feat/<scope>` | New user-visible behavior |
| `fix/<scope>` | Bug fixes |
| `refactor/<scope>` | Code structure changes, no behavior change |
| `chore/<scope>` | Tooling, deps, CI, cleanup |
| `docs/<scope>` | Documentation only |
Keep the slug short and kebab-case, e.g. `fix/r8-animation-release`, `chore/koin-application-migration`.
## Rebase Variant
When the user says *"rebase #NNNN"* or *"dust off PR NNNN"*:
```bash
git fetch upstream --prune
gh pr checkout <NNNN> # checks out the PR head locally
git rebase upstream/main
git submodule update --init --recursive
# Resolve conflicts, then:
git push --force-with-lease
```
Never use plain `--force`. Always `--force-with-lease` to avoid clobbering collaborator pushes.
## Post-Branch Checklist
- [ ] Branch name follows conventional prefix.
- [ ] Submodules up to date.
- [ ] `local.properties` exists.
- [ ] `ANDROID_HOME` exported (see AGENTS.md workspace bootstrap).
- [ ] Optional: run `./gradlew assembleDebug` once to catch environment regressions before editing.
## Tip: Prefer `/delegate` for Long Audits
If the user's opening prompt is a sweeping audit or investigation (e.g. *"audit changes since
v2.7.13 for regressions"*, *"investigate why animations are broken on release"*), consider
suggesting `/delegate` — the GitHub cloud agent can execute the branch + investigation + PR
end-to-end while the user keeps working locally. See AGENTS.md `<copilot_cli_workflow>`.

View file

@ -18,7 +18,6 @@ You are an expert Android and Kotlin Multiplatform (KMP) engineer working on Mes
- `.skills/testing-ci/` - Validation commands, CI pipeline architecture, CI Gradle properties.
- `.skills/implement-feature/` - Step-by-step feature workflow.
- `.skills/code-review/` - PR validation checklist.
- `.skills/new-branch/` - Canonical recipe for branching off upstream/main and rebasing stale PRs.
- **Active Status:** Read `docs/kmp-status.md` and `docs/roadmap.md` to understand the current KMP migration epoch.
</context_and_memory>
@ -74,33 +73,6 @@ Do NOT duplicate content into agent-specific files. When you modify architecture
- **Read Before Refactoring:** When a pattern contradicts best practices, analyze whether it is legacy debt or a deliberate architectural choice before proposing a change.
</rules>
<copilot_cli_workflow>
These tips apply when the agent is the GitHub Copilot CLI. Other agent runtimes may ignore this
section.
- **Delegate long autonomous work.** For sweeping audits, multi-hour investigations, or "fleet"
prompts (*"investigate why X is broken on release"*, *"audit the diff since tag vX.Y.Z"*,
*"review the codebase for best practices against spec Z"*), prefer `/delegate` so the GitHub
cloud agent opens a PR while the user keeps working locally. Don't tie up an interactive
session on work that can run unattended.
- **Use `/research` for "latest hotness" prompts.** When the user asks for *"the latest scoop"*
on Kotlin / KMP / Compose / Koin trends, the built-in `/research` slash command performs deep
research across GitHub and the web with better source grounding than an ad-hoc prompt.
- **Use `/plan` mode for "noodle it out" prompts.** When the user asks for an implementation
plan, a "walk me through next steps", or explicitly says "don't do anything yet" — switch to
plan mode (Shift+Tab or `/plan`). Plans persist in the session workspace and keep the agent
from prematurely editing files. Continue to write long-form plans and Mermaid diagrams to
`.agent_plans/` (git-ignored) for multi-module refactors.
- **`/share` audit and review outputs.** After large audits, PR safety reviews, or release-cycle
quality passes, offer `/share` to export the findings to a gist or markdown file. These
reports are valuable artifacts — don't let them die in session history.
- **Prefer `/rewind` or `ctrl+s` over retyping.** If a turn went sideways, `/rewind` reverts
file changes and the turn; `ctrl+s` submits while preserving the input for quick iteration.
Avoid re-issuing the same prompt verbatim.
- **New-branch flow lives in a skill.** When the user says "fresh branch off fetched origin/main"
or "rebase PR #NNNN", consult `.skills/new-branch/SKILL.md` rather than re-deriving the recipe.
</copilot_cli_workflow>
<git_and_prs>
- **Commit Hygiene:** Squash fixup/polish/review-feedback commits before opening a PR. Each commit should represent a logical, self-contained unit of work — not a back-and-forth conversation.
- **PR Descriptions:** Keep PR descriptions concise and scannable. State *what changed* and *why*, not a per-commit play-by-play. Use a short summary paragraph followed by a bullet list of changes. Avoid tables, headers-per-commit, or verbose breakdowns. Reference the `meshtastic/firmware` repo PRs for tone and style.

View file

@ -6,4 +6,4 @@
- **Think First:** Always outline your step-by-step reasoning inside `<thinking>` tags before writing code or shell commands. Claude models perform significantly better on complex KMP tasks when they "think out loud" first.
- **Skills:** The `.skills/` directory contains task-specific instruction modules. Load them as needed — only the skill relevant to your current task.
- **Plan Mode:** Use plan mode for architectural changes spanning multiple modules. Write plans to `.agent_plans/` (git-ignored). The Copilot-CLI-specific `/plan`, `/delegate`, `/research`, and `/share` guidance in `AGENTS.md` does not apply to Claude Code — skip the `<copilot_cli_workflow>` section.
- **Plan Mode:** Use plan mode for architectural changes spanning multiple modules. Write plans to `.agent_plans/` (git-ignored).

View file

@ -3,13 +3,13 @@ GEM
specs:
CFPropertyList (3.0.8)
abbrev (0.1.2)
addressable (2.9.0)
addressable (2.8.8)
public_suffix (>= 2.0.2, < 8.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.4.0)
aws-partitions (1.1240.0)
aws-sdk-core (3.245.0)
aws-partitions (1.1213.0)
aws-sdk-core (3.242.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
@ -17,11 +17,11 @@ GEM
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.123.0)
aws-sdk-core (~> 3, >= 3.244.0)
aws-sdk-kms (1.121.0)
aws-sdk-core (~> 3, >= 3.241.4)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.219.0)
aws-sdk-core (~> 3, >= 3.244.0)
aws-sdk-s3 (1.213.0)
aws-sdk-core (~> 3, >= 3.241.4)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
@ -29,7 +29,7 @@ GEM
babosa (1.0.4)
base64 (0.2.0)
benchmark (0.5.0)
bigdecimal (4.1.2)
bigdecimal (4.0.1)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
@ -68,11 +68,11 @@ GEM
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.4)
faraday-retry (1.0.3)
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.1)
fastlane (2.233.0)
fastimage (2.4.0)
fastlane (2.232.2)
CFPropertyList (>= 2.3, < 4.0.0)
abbrev (~> 0.1.2)
addressable (>= 2.8, < 3.0.0)
@ -92,7 +92,7 @@ GEM
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
fastlane-sirp (>= 1.1.0)
fastlane-sirp (>= 1.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
@ -122,9 +122,10 @@ GEM
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.4.1)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-sirp (1.1.0)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.99.0)
google-apis-androidpublisher_v3 (0.95.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-core (0.18.0)
addressable (~> 2.5, >= 2.5.1)
@ -138,15 +139,15 @@ GEM
google-apis-core (>= 0.15.0, < 2.a)
google-apis-playcustomapp_v1 (0.17.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-storage_v1 (0.61.0)
google-apis-storage_v1 (0.59.0)
google-apis-core (>= 0.15.0, < 2.a)
google-cloud-core (1.8.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (2.1.1)
faraday (>= 1.0, < 3.a)
google-cloud-errors (1.6.0)
google-cloud-storage (1.59.0)
google-cloud-errors (1.5.0)
google-cloud-storage (1.58.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-core (>= 0.18, < 2)
@ -168,13 +169,13 @@ GEM
httpclient (2.9.0)
mutex_m
jmespath (1.6.2)
json (2.19.4)
json (2.18.1)
jwt (2.10.2)
base64
logger (1.7.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
multi_json (1.20.1)
multi_json (1.19.1)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
@ -184,13 +185,13 @@ GEM
os (1.1.4)
ostruct (0.6.3)
plist (3.7.2)
public_suffix (7.0.5)
rake (13.4.2)
public_suffix (7.0.2)
rake (13.3.1)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.4.1)
retriable (3.1.2)
rexml (3.4.4)
rouge (3.28.0)
ruby2_keywords (0.0.5)
@ -204,6 +205,7 @@ GEM
simctl (1.6.10)
CFPropertyList
naturally
sysrandom (1.0.5)
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)

View file

@ -279,6 +279,7 @@ dependencies {
googleImplementation(libs.maps.compose)
googleImplementation(libs.maps.compose.utils)
googleImplementation(libs.maps.compose.widgets)
googleImplementation(libs.dd.sdk.android.compose)
googleImplementation(libs.dd.sdk.android.logs)
googleImplementation(libs.dd.sdk.android.rum)
googleImplementation(libs.dd.sdk.android.session.replay)

View file

@ -1,45 +1,65 @@
# ============================================================================
# Meshtastic Android ProGuard / R8 rules for release minification
# ============================================================================
# Open-source project: obfuscation and optimization are disabled. We rely on
# tree-shaking (unused code removal) for APK size reduction.
#
# Cross-platform library rules (Koin, kotlinx-serialization, Wire, Room,
# Ktor, Coil, Kable, Kermit, Okio, DataStore, Paging, Lifecycle, Navigation 3,
# AboutLibraries, Markdown, QRCode, CMP resources, core model) live in
# config/proguard/shared-rules.pro and are wired in by the
# AndroidApplicationConventionPlugin. This file holds only Android-specific
# rules and R8-only directives.
# Open-source project: obfuscation is disabled. We rely on tree-shaking and
# code optimization for APK size reduction.
# ============================================================================
# ---- General ----------------------------------------------------------------
# Preserve line numbers for meaningful crash stack traces
-keepattributes SourceFile,LineNumberTable
# Open-source no need to obfuscate
-dontobfuscate
# Disable R8 optimization passes. Tree-shaking (unused code removal) still
# runs only method-body rewrites and call-site transformations are suppressed.
#
# Why: CMP 1.11 ships consumer rules with -assumenosideeffects on
# Composer.<clinit>() and ComposerImpl.<clinit>(), plus -assumevalues on
# ComposeRuntimeFlags and ComposeStackTraceMode. These optimization directives
# let R8 rewrite *call sites* (class-init triggers, flag reads) even when the
# target classes are preserved by -keep rules. The result is that the Compose
# recomposer/frame-clock/animation state machines silently freeze on their
# first frame in release builds. -dontoptimize is the only directive that
# disables processing of -assumenosideeffects/-assumevalues. See #5146.
-dontoptimize
# Dump the full merged R8 configuration (app rules + all library consumer rules)
# for auditing. Inspect this file after a release build to see what libraries inject.
-printconfiguration build/outputs/mapping/r8-merged-config.txt
# ---- Networking (transitive references from Ktor on Android) ----------------
# ---- Networking (transitive references from Ktor) ---------------------------
-dontwarn org.conscrypt.**
-dontwarn org.bouncycastle.**
-dontwarn org.openjsse.**
# Compose runtime/ui/animation/foundation/material3 keep rules now live in
# config/proguard/shared-rules.pro so both Android (R8) and desktop (ProGuard)
# get the same defence-in-depth coverage against CMP 1.11 optimizer folding.
# ---- Wire Protobuf ----------------------------------------------------------
# Wire-generated proto message classes (accessed via ADAPTER companion reflection)
-keep class org.meshtastic.proto.** { *; }
# ---- Room KMP (room3) ------------------------------------------------------
# Preserve generated database constructors (Room uses reflection to instantiate)
-keep class * extends androidx.room3.RoomDatabase { <init>(); }
# ---- Koin DI ----------------------------------------------------------------
# Prevent R8 from merging exception classes (observed as io.ktor.http.URLDecodeException
# replacing Koin's InstanceCreationException in stack traces, making crashes undiagnosable).
-keep class org.koin.core.error.** { *; }
# ---- Compose Runtime & Animation --------------------------------------------
# R8's optimization passes (bundled with AGP 9.x) can inline and dead-code-
# eliminate parts of the Compose frame-clock / recomposer / animation state
# machines, causing every animation to silently freeze on its first frame in
# release builds indeterminate progress spinners, crossfade transitions,
# animateFloatAsState, AnimatedVisibility, etc.
#
# The frame clock lives in compose.runtime, the draw loop in compose.ui,
# and the animation drivers in compose.animation.core. Keep all three so
# R8 does not break the chain.
-keep class androidx.compose.runtime.** { *; }
-keep class androidx.compose.ui.** { *; }
-keep class androidx.compose.animation.core.** { *; }
-keep class androidx.compose.animation.** { *; }
# ---- Compose Multiplatform --------------------------------------------------
# Keep resource library internals and generated Res accessor classes so R8 does
# not tree-shake the resource loading infrastructure. Without these rules the
# fdroid flavor crashes at startup with a misleading URLDecodeException due to
# R8 exception-class merging.
-keep class org.jetbrains.compose.resources.** { *; }
-keep class org.meshtastic.core.resources.** { *; }

View file

@ -861,9 +861,15 @@ private fun CacheInfoDialog(mapView: MapView, onDismiss: () -> Unit) {
onDismiss = onDismiss,
negativeButton = { TextButton(onClick = { onDismiss() }) { Text(text = stringResource(Res.string.close)) } },
) {
val capacityMb = (cacheCapacity / (1024 * 1024)).toLong()
val usageMb = (currentCacheUsage / (1024 * 1024)).toLong()
Text(modifier = Modifier.padding(16.dp), text = stringResource(Res.string.map_cache_info, capacityMb, usageMb))
Text(
modifier = Modifier.padding(16.dp),
text =
stringResource(
Res.string.map_cache_info,
cacheCapacity / (1024.0 * 1024.0),
currentCacheUsage / (1024.0 * 1024.0),
),
)
}
}

View file

@ -26,6 +26,7 @@ import co.touchlab.kermit.LogWriter
import co.touchlab.kermit.Severity
import com.datadog.android.Datadog
import com.datadog.android.DatadogSite
import com.datadog.android.compose.enableComposeActionTracking
import com.datadog.android.core.configuration.Configuration
import com.datadog.android.log.Logger
import com.datadog.android.log.Logs
@ -159,6 +160,7 @@ class GooglePlatformAnalytics(private val context: Context, private val analytic
.trackFrustrations(false) // Disable click-tracking based frustration detection
.trackLongTasks()
.trackNonFatalAnrs(true)
.enableComposeActionTracking() // Required: activates runtime consumption of Compose semantics tags
.setSessionSampleRate(sampleRate)
.build()
Rum.enable(rumConfiguration)

View file

@ -59,7 +59,6 @@ import org.meshtastic.app.node.metrics.getTracerouteMapOverlayInsets
import org.meshtastic.app.ui.MainScreen
import org.meshtastic.core.barcode.rememberBarcodeScanner
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
import org.meshtastic.core.network.repository.UsbRepository
import org.meshtastic.core.nfc.NfcScannerEffect
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.channel_invalid
@ -92,8 +91,6 @@ import org.meshtastic.feature.node.metrics.TracerouteMapScreen
class MainActivity : ComponentActivity() {
private val model: UIViewModel by viewModel()
private val usbRepository: UsbRepository by inject()
/**
* Activity-lifecycle-aware client that binds to the mesh service. Note: This is used implicitly as it registers
* itself as a LifecycleObserver in its init block.
@ -169,16 +166,6 @@ class MainActivity : ComponentActivity() {
handleIntent(intent)
}
override fun onResume() {
super.onResume()
// Belt-and-suspenders for the Android 12+ attach-intent quirk: if the activity is
// resumed while a USB device is already attached (e.g. process restart, returning
// from another app), the manifest-declared attach intent may have already fired
// before UsbRepository was constructed. Re-poll deviceList here so the UI reflects
// reality without requiring the user to physically replug.
usbRepository.refreshState()
}
@Composable
private fun AppCompositionLocals(content: @Composable () -> Unit) {
CompositionLocalProvider(
@ -270,11 +257,6 @@ class MainActivity : ComponentActivity() {
UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
Logger.d { "USB device attached" }
// Android 12+ delivers ACTION_USB_DEVICE_ATTACHED only to manifest-declared
// receivers, so the runtime-registered UsbBroadcastReceiver inside UsbRepository
// never sees this event. Forward it explicitly so the serialDevices StateFlow
// refreshes and the device shows up in the Connect → Serial tab.
usbRepository.refreshState()
showSettingsPage()
}

View file

@ -37,8 +37,9 @@ import kotlinx.coroutines.withTimeout
import org.koin.android.ext.android.get
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.workmanager.koin.workManagerFactory
import org.koin.plugin.module.dsl.startKoin
import org.meshtastic.app.di.AndroidKoinApp
import org.koin.core.context.startKoin
import org.meshtastic.app.di.AppKoinModule
import org.meshtastic.app.di.module
import org.meshtastic.core.common.ContextServices
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.repository.MeshPrefs
@ -63,9 +64,10 @@ open class MeshUtilApplication :
super.onCreate()
ContextServices.app = this
startKoin<AndroidKoinApp> {
startKoin {
androidContext(this@MeshUtilApplication)
workManagerFactory()
modules(AppKoinModule().module())
}
// Schedule periodic MeshLog cleanup

View file

@ -25,7 +25,6 @@ import androidx.work.WorkerParameters
import io.ktor.client.HttpClient
import io.ktor.client.engine.HttpClientEngine
import kotlinx.coroutines.CoroutineDispatcher
import org.koin.plugin.module.dsl.koinApplication
import org.koin.test.verify.definition
import org.koin.test.verify.injectedParameters
import org.koin.test.verify.verify
@ -61,19 +60,4 @@ class KoinVerificationTest {
),
)
}
@Test
fun verifyTypedBootstrapLoadsModuleGraph() {
// koinApplication<T>() is a K2 compiler plugin stub. If the plugin fails to
// transform it, the stub throws NotImplementedError at runtime. This test
// validates that the production bootstrap path is correctly transformed by
// successfully creating and closing the generated Koin application.
val app = koinApplication<AndroidKoinApp>()
try {
// No-op: reaching this point proves the typed bootstrap path did not
// throw and the generated application could be created.
} finally {
app.close()
}
}
}

View file

@ -54,6 +54,7 @@ dependencies {
compileOnly(libs.kotlin.gradlePlugin)
compileOnly(libs.ksp.gradlePlugin)
compileOnly(libs.androidx.room.gradlePlugin)
compileOnly(libs.secrets.gradlePlugin)
compileOnly(libs.spotless.gradlePlugin)
compileOnly(libs.test.retry.gradlePlugin)

View file

@ -18,7 +18,7 @@ import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.datadog.gradle.plugin.DdExtension
import com.datadog.gradle.plugin.InjectBuildIdToAssetsTask
import com.datadog.gradle.plugin.InstrumentationMode
import com.datadog.gradle.plugin.SdkCheckLevel
import org.gradle.api.Plugin
import org.gradle.api.Project
@ -110,7 +110,7 @@ class AnalyticsConventionPlugin : Plugin<Project> {
variants {
register(variant.name) {
site = "US5"
composeInstrumentation = InstrumentationMode.AUTO
}
}
checkProjectDependencies = SdkCheckLevel.NONE

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,6 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import com.android.build.api.dsl.ApplicationExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
@ -25,6 +26,7 @@ import org.meshtastic.buildlogic.configureTestOptions
class AndroidApplicationConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
apply(plugin = "com.android.application")
apply(plugin = "org.gradle.test-retry")
apply(plugin = "meshtastic.android.lint")
@ -36,8 +38,11 @@ class AndroidApplicationConventionPlugin : Plugin<Project> {
extensions.configure<ApplicationExtension> {
configureKotlinAndroid(this)
defaultConfig {
vectorDrawables.useSupportLibrary = true
}
defaultConfig { vectorDrawables.useSupportLibrary = true }
buildTypes {
getByName("release") {
@ -45,8 +50,7 @@ class AndroidApplicationConventionPlugin : Plugin<Project> {
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
rootProject.file("config/proguard/shared-rules.pro"),
"proguard-rules.pro",
"proguard-rules.pro"
)
}
getByName("debug") {
@ -58,7 +62,9 @@ class AndroidApplicationConventionPlugin : Plugin<Project> {
}
}
buildFeatures { buildConfig = true }
buildFeatures {
buildConfig = true
}
}
configureTestOptions()
}

View file

@ -32,12 +32,10 @@ class KmpLibraryComposeConventionPlugin : Plugin<Project> {
apply(plugin = libs.plugin("compose-multiplatform").get().pluginId)
extensions.configure<KotlinMultiplatformExtension> {
sourceSets.matching { it.name == "commonMain" }.configureEach {
dependencies {
implementation(libs.library("compose-multiplatform-runtime"))
// API because consuming modules will usually need the resource types
api(libs.library("compose-multiplatform-resources"))
}
sourceSets.getByName("commonMain").dependencies {
implementation(libs.library("compose-multiplatform-runtime"))
// API because consuming modules will usually need the resource types
api(libs.library("compose-multiplatform-resources"))
}
}
configureComposeCompiler()

View file

@ -14,9 +14,11 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import dev.mokkery.gradle.MokkeryGradleExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure
import org.meshtastic.buildlogic.configureAndroidMarketplaceFallback
import org.meshtastic.buildlogic.configureKmpTestDependencies
import org.meshtastic.buildlogic.configureKotlinMultiplatform
@ -37,6 +39,8 @@ class KmpLibraryConventionPlugin : Plugin<Project> {
apply(plugin = "org.gradle.test-retry")
apply(plugin = libs.plugin("mokkery").get().pluginId)
extensions.configure<MokkeryGradleExtension> { stubs.allowConcreteClassInstantiation.set(true) }
configureKotlinMultiplatform()
configureKmpTestDependencies()
configureTestOptions()

View file

@ -29,12 +29,11 @@ class KoinConventionPlugin : Plugin<Project> {
// Configure Koin K2 Compiler Plugin (0.4.0+)
extensions.configure(KoinGradleExtension::class.java) {
// Meshtastic uses dependency inversion across KMP modules — interfaces in
// commonMain, implementations wired at the composition root. Koin's compileSafety
// flag enables A1 per-module checks that treat every module as self-contained,
// which breaks this pattern. There is no separate flag for A3 full-graph
// validation. Until Koin exposes granular safety levels we keep this disabled;
// runtime graph verification is handled by KoinVerificationTest instead.
// Meshtastic heavily utilizes dependency inversion across KMP modules. Koin's A1
// per-module safety checks strictly enforce that all dependencies must be explicitly
// provided or included locally. This breaks decoupled Clean Architecture designs.
// We disable compile safety globally to properly rely on Koin's A3 full-graph
// validation which perfectly handles inverted dependencies at the composition root.
compileSafety.set(false)
}

View file

@ -77,9 +77,7 @@ internal fun Project.configureKotlinMultiplatform() {
// upgrades to the CMP-bundled version, triggering a "Skiko dependencies'
// versions are incompatible" warning from CMP's compatibility checker.
// Force the version to match CMP so the checker sees a consistent graph.
// Pinned here rather than in the version catalog because this plugin is the
// only consumer — bump together with the compose-multiplatform version.
val skikoVersion = "0.144.5"
val skikoVersion = libs.version("skiko")
configurations.configureEach {
resolutionStrategy.eachDependency {
if (requested.group == "org.jetbrains.skiko") {

View file

@ -30,7 +30,7 @@ pluginManagement {
}
plugins {
id("com.gradle.develocity") version("4.4.1")
id("com.gradle.develocity") version("4.4.0")
}
dependencyResolutionManagement {

View file

@ -1,166 +0,0 @@
# ============================================================================
# Meshtastic Shared ProGuard / R8 rules
# ============================================================================
# Cross-platform keep and dontwarn rules applied to BOTH the Android app
# release build (R8) and the Desktop distribution (ProGuard). Host-specific
# rules live in the per-module proguard-rules.pro file.
#
# Rule of thumb: anything describing a library shared between Android and
# Desktop (Koin, kotlinx-serialization, Wire, Room KMP, Ktor, Coil 3, Kable,
# Kermit, Okio, DataStore, Paging, Lifecycle / Navigation 3, AboutLibraries,
# Markdown renderer, QRCode, Compose Multiplatform resources, core modules)
# belongs here. Anything platform-specific (AWT/Skiko/JNA, AIDL, Android
# framework, JDK-version quirks, flavor specifics) stays in the host file.
# ============================================================================
# ---- Attributes -------------------------------------------------------------
# Preserve line numbers for meaningful stack traces, plus metadata needed for
# reflective serializer/DI/Room lookups.
-keepattributes SourceFile,LineNumberTable,*Annotation*,Signature,InnerClasses,EnclosingMethod,Exceptions,RuntimeVisibleAnnotations
# ---- Kotlin / Coroutines ----------------------------------------------------
# Kotlin stdlib and kotlinx-coroutines ship their own consumer ProGuard rules
# (kotlin-stdlib and kotlinx-coroutines-core consumer-rules.pro) which keep
# Metadata, Continuation, kotlin.reflect internals, and debug metadata. No
# explicit wildcards needed here.
# ---- Koin DI (reflection-based injection) -----------------------------------
# Prevent R8 from merging exception classes (observed as io.ktor.http.URLDecodeException
# replacing Koin's InstanceCreationException in stack traces, making crashes
# undiagnosable). Broadened to all of koin core to cover the KSP-generated graph.
-keep class org.koin.** { *; }
-dontwarn org.koin.**
# Keep Koin-annotated modules/components so Koin Annotations (KSP) output
# survives tree-shaking.
-keep @org.koin.core.annotation.Module class * { *; }
-keep @org.koin.core.annotation.ComponentScan class * { *; }
-keep @org.koin.core.annotation.Single class * { *; }
-keep @org.koin.core.annotation.Factory class * { *; }
-keep @org.koin.core.annotation.KoinViewModel class * { *; }
# ---- kotlinx-serialization --------------------------------------------------
-keep class kotlinx.serialization.** { *; }
-dontwarn kotlinx.serialization.**
# Keep @Serializable classes and their generated $serializer companions
-keepclassmembers @kotlinx.serialization.Serializable class ** {
static ** Companion;
kotlinx.serialization.KSerializer serializer(...);
}
-keep class **.$serializer { *; }
-keepclassmembers class **.$serializer { *; }
-keepclasseswithmembers class ** {
kotlinx.serialization.KSerializer serializer(...);
}
# ---- Wire Protobuf ----------------------------------------------------------
# Wire generates an ADAPTER static field on every Message subclass accessed
# reflectively during encoding/decoding. Keep those fields and the
# ProtoAdapter subclasses themselves; Wire's bundled consumer rules preserve
# the runtime itself.
-keepclassmembers class * extends com.squareup.wire.Message {
public static *** ADAPTER;
}
-keepclassmembers class * extends com.squareup.wire.ProtoAdapter { *; }
# Suppress warnings about missing Android Parcelable (Wire cross-platform stubs
# when compiling for non-Android JVM targets; harmless on Android).
-dontwarn android.os.Parcel**
-dontwarn android.os.Parcelable**
# ---- Room KMP (room3) -------------------------------------------------------
# Preserve generated database constructors (Room uses reflection to instantiate)
-keep class * extends androidx.room3.RoomDatabase { <init>(); }
-keep class * implements androidx.room3.RoomDatabaseConstructor { *; }
# Keep the expect/actual MeshtasticDatabaseConstructor + database surface
-keep class org.meshtastic.core.database.MeshtasticDatabaseConstructor { *; }
-keep class org.meshtastic.core.database.MeshtasticDatabase { *; }
# Room's own consumer rules (from androidx.room3) keep DAOs, entities,
# generated _Impl classes, and TypeConverters referenced from the database.
# ---- SQLite bundled --------------------------------------------------------
# androidx.sqlite ships consumer rules.
# ---- Ktor (ServiceLoader + plugin discovery) --------------------------------
# Keep ServiceLoader metadata files (ktor discovers HttpClientEngineFactory
# implementations reflectively via ServiceLoader).
-keepclassmembers class * implements io.ktor.client.HttpClientEngineFactory { *; }
# ---- Coil 3 (image loading) -------------------------------------------------
# coil3 ships consumer rules.
# ---- Kable BLE --------------------------------------------------------------
# com.juul.kable ships consumer rules; if release builds fail with missing
# Kable classes, restore a narrow keep for the specific reflection-loaded type.
# ---- Compose Multiplatform resources ----------------------------------------
# Generated resource accessor classes (Res.string.*, Res.drawable.*, etc.).
# Without these the fdroid flavor has crashed at startup with a misleading
# URLDecodeException due to R8 exception-class merging.
-keep class org.jetbrains.compose.resources.** { *; }
-keep class org.meshtastic.core.resources.Res { *; }
-keepclassmembers class org.meshtastic.core.resources.Res$* { *; }
# ---- AboutLibraries ---------------------------------------------------------
# com.mikepenz.aboutlibraries ships consumer rules.
# ---- Multiplatform Markdown Renderer ----------------------------------------
# com.mikepenz.markdown ships consumer rules.
# ---- QR Code Kotlin ---------------------------------------------------------
-keep class io.github.g0dkar.qrcode.** { *; }
-dontwarn io.github.g0dkar.qrcode.**
-keep class qrcode.** { *; }
-dontwarn qrcode.**
# ---- Kermit logging ---------------------------------------------------------
# co.touchlab.kermit ships consumer rules.
# ---- Okio -------------------------------------------------------------------
# okio ships consumer rules.
# ---- DataStore --------------------------------------------------------------
# androidx.datastore ships consumer rules.
# ---- Paging -----------------------------------------------------------------
# androidx.paging ships consumer rules.
# ---- Lifecycle / Navigation 3 / ViewModel (JetBrains forks) -----------------
# androidx.lifecycle and androidx.navigation3 ship consumer rules.
# ---- Meshtastic shared model ------------------------------------------------
# core.model types are reached via static references from Koin-wired graphs,
# Room entities, and kotlinx-serialization @Serializable companions all of
# which have their own keep rules above.
# ---- Compose Runtime & Animation --------------------------------------------
# Defence-in-depth: prevent tree-shaking of Compose infrastructure classes that
# are referenced indirectly through compiler-generated state machines. Applies
# to BOTH R8 (Android app) and ProGuard (desktop distribution).
#
# Why shared: CMP 1.11 ships consumer rules with -assumenosideeffects on
# Composer.<clinit>() / ComposerImpl.<clinit>() and -assumevalues on
# ComposeRuntimeFlags / ComposeStackTraceMode. If the optimizer runs (R8 full
# mode on Android, ProGuard with optimize.set(true) on desktop) these call
# sites can be rewritten even when the target classes are kept, causing the
# recomposer / frame-clock / animation state machines to silently freeze on
# the first frame. -dontoptimize (set per-host) is the primary defence; these
# keep rules are a safety net against future toolchain changes. See #5146.
-keep class androidx.compose.runtime.** { *; }
-keep class androidx.compose.ui.** { *; }
-keep class androidx.compose.animation.core.** { *; }
-keep class androidx.compose.animation.** { *; }
-keep class androidx.compose.foundation.** { *; }
-keep class androidx.compose.material3.** { *; }

View file

@ -26,9 +26,7 @@ import com.juul.kable.UnmetRequirementException
/**
* Classification of a BLE-layer exception for the transport layer to act on.
*
* @property isPermanent `true` if the condition cannot resolve without explicit user re-selection of the device.
* Currently always `false` all known BLE exceptions can resolve without user intervention (BT toggling, permission
* grants, transient GATT errors). Reserved for future use.
* @property isPermanent `true` if the condition won't resolve without user intervention (e.g. Bluetooth disabled).
* @property gattStatus the platform GATT status code when available (Android-specific).
* @property message a human-readable description of the failure.
*/
@ -52,9 +50,6 @@ fun Throwable.classifyBleException(): BleExceptionInfo? = when (this) {
is GattRequestRejectedException ->
BleExceptionInfo(isPermanent = false, message = "GATT request rejected (busy)")
is UnmetRequirementException ->
// Bluetooth disabled or runtime permission missing. Both can resolve without re-selecting the
// device (user re-enables BT, or grants permission). Surface as transient so the transport keeps
// retrying; UI can show a hint based on the message.
BleExceptionInfo(isPermanent = false, message = message ?: "Bluetooth LE unavailable")
BleExceptionInfo(isPermanent = true, message = message ?: "Bluetooth LE unavailable")
else -> null
}

View file

@ -1,39 +0,0 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.common.di
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.ioDispatcher
/**
* A process-wide [CoroutineScope] that outlives individual ViewModels and UI components.
*
* Use this scope for fire-and-forget cleanup work that must continue after a ViewModel's own scope has been cancelled
* (for example, deleting temporary files in `onCleared()`). Backed by a [SupervisorJob] so failures in one child do not
* cancel siblings, and by [ioDispatcher] so work runs off the main thread.
*
* Prefer scoping work to a more specific scope (like `viewModelScope`) whenever possible; this scope is an escape hatch
* and should be used sparingly.
*/
interface ApplicationCoroutineScope : CoroutineScope
@Single(binds = [ApplicationCoroutineScope::class])
internal class ApplicationCoroutineScopeImpl : ApplicationCoroutineScope {
override val coroutineContext = SupervisorJob() + ioDispatcher
}

View file

@ -95,18 +95,3 @@ inline fun <T, R> T.safeCatching(block: T.() -> R): Result<R> = try {
} catch (e: Exception) {
Result.failure(e)
}
/**
* Like [safeCatching] but also catches JVM [Error]s (e.g. [ExceptionInInitializerError] raised by compose-resources'
* lazy skiko initialization on the desktop JVM test classpath). Still re-throws [CancellationException] so structured
* concurrency is preserved. Use when the block invokes code whose failure modes include static-initializer errors and
* the caller only needs a best-effort fallback.
*/
@Suppress("TooGenericExceptionCaught")
inline fun <T> safeCatchingAll(block: () -> T): Result<T> = try {
Result.success(block())
} catch (e: CancellationException) {
throw e
} catch (t: Throwable) {
Result.failure(t)
}

View file

@ -23,7 +23,6 @@ package org.meshtastic.core.common.util
* All methods return locale-independent strings using [NumberFormatter] (dot decimal separator), which is intentional
* for a mesh networking app where consistency matters.
*/
@Suppress("TooManyFunctions")
object MetricFormatter {
fun temperature(celsius: Float, isFahrenheit: Boolean): String {
@ -48,12 +47,6 @@ object MetricFormatter {
fun snr(value: Float, decimalPlaces: Int = 1): String = "${NumberFormatter.format(value, decimalPlaces)} dB"
fun rssi(value: Int): String = "$value dBm"
fun windSpeed(metersPerSecond: Float, decimalPlaces: Int = 1): String =
"${NumberFormatter.format(metersPerSecond, decimalPlaces)} m/s"
fun rainfall(millimeters: Float, decimalPlaces: Int = 1): String =
"${NumberFormatter.format(millimeters, decimalPlaces)} mm"
}
private const val FAHRENHEIT_SCALE = 1.8f

View file

@ -120,24 +120,4 @@ class MetricFormatterTest {
fun snrNegative() {
assertEquals("-5.5 dB", MetricFormatter.snr(-5.5f))
}
@Test
fun windSpeed() {
assertEquals("12.3 m/s", MetricFormatter.windSpeed(12.34f))
}
@Test
fun windSpeedZero() {
assertEquals("0.0 m/s", MetricFormatter.windSpeed(0.0f))
}
@Test
fun rainfall() {
assertEquals("2.5 mm", MetricFormatter.rainfall(2.54f))
}
@Test
fun rainfallZero() {
assertEquals("0.0 mm", MetricFormatter.rainfall(0.0f))
}
}

View file

@ -45,7 +45,6 @@ import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.repository.PlatformAnalytics
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.ServiceBroadcasts
import org.meshtastic.core.repository.UiPrefs
import org.meshtastic.proto.AdminMessage
import org.meshtastic.proto.Channel
import org.meshtastic.proto.Config
@ -64,7 +63,6 @@ class MeshActionHandlerImpl(
private val dataHandler: Lazy<MeshDataHandler>,
private val analytics: PlatformAnalytics,
private val meshPrefs: MeshPrefs,
private val uiPrefs: UiPrefs,
private val databaseManager: DatabaseManager,
private val notificationManager: NotificationManager,
private val messageProcessor: Lazy<MeshMessageProcessor>,
@ -209,7 +207,7 @@ class MeshActionHandlerImpl(
override fun handleRequestPosition(destNum: Int, position: Position, myNodeNum: Int) {
if (destNum != myNodeNum) {
val provideLocation = uiPrefs.shouldProvideNodeLocation(myNodeNum).value
val provideLocation = meshPrefs.shouldProvideNodeLocation(myNodeNum).value
val currentPosition =
when {
provideLocation && position.isValid() -> position

View file

@ -60,7 +60,6 @@ import org.meshtastic.proto.AdminMessage
import org.meshtastic.proto.Config
import org.meshtastic.proto.Telemetry
import org.meshtastic.proto.ToRadio
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit
@ -212,11 +211,11 @@ class MeshConnectionManagerImpl(
}
}
private fun startHandshakeStallGuard(stage: Int, timeout: Duration, action: () -> Unit) {
private fun startHandshakeStallGuard(stage: Int, action: () -> Unit) {
handshakeTimeout?.cancel()
handshakeTimeout =
scope.handledLaunch {
delay(timeout)
delay(HANDSHAKE_TIMEOUT)
if (serviceRepository.connectionState.value is ConnectionState.Connecting) {
// Attempt one retry. Note: the firmware silently drops identical consecutive
// writes (per-connection dedup). If the first want_config_id was received and
@ -292,13 +291,13 @@ class MeshConnectionManagerImpl(
override fun startConfigOnly() {
val action = { packetHandler.sendToRadio(ToRadio(want_config_id = HandshakeConstants.CONFIG_NONCE)) }
startHandshakeStallGuard(1, HANDSHAKE_TIMEOUT_STAGE1, action)
startHandshakeStallGuard(1, action)
action()
}
override fun startNodeInfoOnly() {
val action = { packetHandler.sendToRadio(ToRadio(want_config_id = HandshakeConstants.NODE_INFO_NONCE)) }
startHandshakeStallGuard(2, HANDSHAKE_TIMEOUT_STAGE2, action)
startHandshakeStallGuard(2, action)
action()
}
@ -405,14 +404,7 @@ class MeshConnectionManagerImpl(
*/
private const val PRE_HANDSHAKE_SETTLE_MS = 100L
private val HANDSHAKE_TIMEOUT_STAGE1 = 30.seconds
/**
* Stage 2 drains the full node database, which can be significantly larger than Stage 1 config on big meshes.
* 60 s matches the meshtastic-client SDK timeout and avoids premature stall-guard triggers on meshes with 50+
* nodes.
*/
private val HANDSHAKE_TIMEOUT_STAGE2 = 60.seconds
private val HANDSHAKE_TIMEOUT = 30.seconds
// Shorter window for the retry attempt: if the device genuinely didn't receive the
// first want_config_id the retry completes within a few seconds. Waiting another 30s

View file

@ -32,8 +32,6 @@ import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.model.MeshLog
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.util.isLora
import org.meshtastic.core.model.util.toOneLineString
import org.meshtastic.core.model.util.toPIIString
import org.meshtastic.core.repository.FromRadioPacketHandler
import org.meshtastic.core.repository.MeshLogRepository
import org.meshtastic.core.repository.MeshMessageProcessor
@ -127,11 +125,11 @@ class MeshMessageProcessorImpl(
proto.xmodemPacket != null -> "XmodemPacket" to proto.xmodemPacket.toString()
proto.deviceuiConfig != null -> "DeviceUIConfig" to proto.deviceuiConfig.toString()
proto.fileInfo != null -> "FileInfo" to proto.fileInfo.toString()
proto.my_info != null -> "MyInfo" to proto.my_info!!.toOneLineString()
proto.node_info != null -> "NodeInfo" to proto.node_info!!.toPIIString()
proto.config != null -> "Config" to proto.config!!.toOneLineString()
proto.moduleConfig != null -> "ModuleConfig" to proto.moduleConfig!!.toOneLineString()
proto.channel != null -> "Channel" to proto.channel!!.toOneLineString()
proto.my_info != null -> "MyInfo" to proto.my_info.toString()
proto.node_info != null -> "NodeInfo" to proto.node_info.toString()
proto.config != null -> "Config" to proto.config.toString()
proto.moduleConfig != null -> "ModuleConfig" to proto.moduleConfig.toString()
proto.channel != null -> "Channel" to proto.channel.toString()
proto.clientNotification != null -> "ClientNotification" to proto.clientNotification.toString()
else -> return
}

View file

@ -20,28 +20,15 @@ import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import org.koin.core.annotation.Named
import org.koin.core.annotation.Single
import org.meshtastic.core.model.MqttConnectionState
import org.meshtastic.core.model.MqttProbeStatus
import org.meshtastic.core.network.repository.MQTTRepository
import org.meshtastic.core.network.repository.resolveEndpoint
import org.meshtastic.core.repository.MqttManager
import org.meshtastic.core.repository.PacketHandler
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.mqtt.ConnectionState
import org.meshtastic.mqtt.MqttClient
import org.meshtastic.mqtt.MqttException
import org.meshtastic.mqtt.ProbeResult
import org.meshtastic.mqtt.probe
import org.meshtastic.proto.MqttClientProxyMessage
import org.meshtastic.proto.ToRadio
@ -53,30 +40,18 @@ class MqttManagerImpl(
@Named("ServiceScope") private val scope: CoroutineScope,
) : MqttManager {
private var mqttMessageFlow: Job? = null
private val proxyActive = MutableStateFlow(false)
override val mqttConnectionState: StateFlow<MqttConnectionState> =
combine(proxyActive, mqttRepository.connectionState) { active, libState ->
if (!active) MqttConnectionState.Inactive else libState.toAppState()
}
.stateIn(scope, SharingStarted.Eagerly, MqttConnectionState.Inactive)
override fun startProxy(enabled: Boolean, proxyToClientEnabled: Boolean) {
if (mqttMessageFlow?.isActive == true) return
if (enabled && proxyToClientEnabled) {
proxyActive.value = true
mqttMessageFlow =
mqttRepository.proxyMessageFlow
.onEach { message -> packetHandler.sendToRadio(ToRadio(mqttClientProxyMessage = message)) }
.catch { throwable ->
proxyActive.value = false
val message =
when (throwable) {
is MqttException.ConnectionRejected -> "MQTT: connection rejected (check credentials)"
is MqttException.ConnectionLost -> "MQTT: connection lost"
else -> "MQTT proxy failed: ${throwable.message}"
}
serviceRepository.setErrorMessage(text = message, severity = Severity.Warn)
serviceRepository.setErrorMessage(
text = "MqttClientProxy failed: $throwable",
severity = Severity.Warn,
)
}
.launchIn(scope)
}
@ -88,7 +63,6 @@ class MqttManagerImpl(
mqttMessageFlow?.cancel()
mqttMessageFlow = null
}
proxyActive.value = false
}
override fun handleMqttProxyMessage(message: MqttClientProxyMessage) {
@ -105,57 +79,4 @@ class MqttManagerImpl(
else -> {}
}
}
private fun ConnectionState.toAppState(): MqttConnectionState = when (this) {
is ConnectionState.Connecting -> MqttConnectionState.Connecting
is ConnectionState.Connected -> MqttConnectionState.Connected
is ConnectionState.Reconnecting ->
MqttConnectionState.Reconnecting(attempt = attempt, lastError = lastError?.message)
is ConnectionState.Disconnected ->
reason?.let { MqttConnectionState.Disconnected(reason = it.message) }
?: MqttConnectionState.Disconnected.Idle
}
override suspend fun probe(
address: String,
tlsEnabled: Boolean,
username: String?,
password: String?,
): MqttProbeStatus {
val endpoint = resolveEndpoint(address, tlsEnabled)
val result =
MqttClient.probe(endpoint = endpoint) {
val user = username?.takeUnless { it.isEmpty() }
val pass = password?.takeUnless { it.isEmpty() }
if (user != null) this.username = user
if (pass != null) password(pass)
}
return result.toAppStatus()
}
private fun ProbeResult.toAppStatus(): MqttProbeStatus = when (this) {
is ProbeResult.Success -> {
val info = serverInfo
val summary =
buildList {
info.assignedClientIdentifier?.let { add("client=$it") }
info.maximumQosOrdinal?.let { add("maxQoS=$it") }
info.serverKeepAliveSeconds?.let { add("keepalive=${it}s") }
}
.joinToString(", ")
.ifEmpty { null }
MqttProbeStatus.Success(serverInfo = summary)
}
is ProbeResult.Rejected ->
MqttProbeStatus.Rejected(
reasonCode = reasonCode.value,
reason = message,
serverReference = serverReference,
)
is ProbeResult.DnsFailure -> MqttProbeStatus.DnsFailure(message = cause.message)
is ProbeResult.TcpFailure -> MqttProbeStatus.TcpFailure(message = cause.message)
is ProbeResult.TlsFailure -> MqttProbeStatus.TlsFailure(message = cause.message)
is ProbeResult.Timeout -> MqttProbeStatus.Timeout(timeoutMs = durationMs)
is ProbeResult.Other -> MqttProbeStatus.Other(message = cause.message)
}
}

View file

@ -22,9 +22,7 @@ import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@ -75,11 +73,6 @@ class PacketHandlerImpl(
private val queueMutex = Mutex()
private val queuedPackets = mutableListOf<MeshPacket>()
// Unbounded channel preserves FIFO ordering of fire-and-forget sendToRadio(MeshPacket)
// calls. The non-suspend entry point does trySend (always succeeds for UNLIMITED) and
// a single consumer coroutine enqueues packets under queueMutex in arrival order.
private val outboundChannel = Channel<MeshPacket>(Channel.UNLIMITED)
// Set to true by stopPacketQueue() under queueMutex. Checked by startPacketQueueLocked()
// and the queue processor's finally block to prevent restarting a stopped queue.
private var queueStopped = false
@ -87,20 +80,6 @@ class PacketHandlerImpl(
private val responseMutex = Mutex()
private val queueResponse = mutableMapOf<Int, CompletableDeferred<Boolean>>()
init {
// Single consumer serializes enqueues from the non-suspend sendToRadio(MeshPacket)
// entry point, preserving FIFO across rapid concurrent callers.
scope.launch {
outboundChannel.consumeAsFlow().collect { packet ->
queueMutex.withLock {
queueStopped = false // Allow queue to resume after a disconnect/reconnect cycle.
queuedPackets.add(packet)
startPacketQueueLocked()
}
}
}
}
override fun sendToRadio(p: ToRadio) {
Logger.d { "Sending to radio ${p.toPIIString()}" }
val b = p.encode()
@ -125,9 +104,13 @@ class PacketHandlerImpl(
}
override fun sendToRadio(packet: MeshPacket) {
// Non-suspend entry point — order-preserving via unbounded channel drained by
// a single consumer coroutine. trySend on UNLIMITED never fails for capacity.
outboundChannel.trySend(packet)
scope.launch {
queueMutex.withLock {
queueStopped = false // Allow queue to resume after a disconnect/reconnect cycle.
queuedPackets.add(packet)
startPacketQueueLocked()
}
}
}
@Suppress("TooGenericExceptionCaught", "SwallowedException")

View file

@ -28,7 +28,6 @@ import kotlinx.coroutines.withContext
import okio.ByteString.Companion.toByteString
import org.koin.core.annotation.Single
import org.meshtastic.core.database.DatabaseProvider
import org.meshtastic.core.database.dao.NodeInfoDao
import org.meshtastic.core.database.entity.PacketEntity
import org.meshtastic.core.database.entity.toReaction
import org.meshtastic.core.di.CoroutineDispatchers
@ -243,10 +242,7 @@ class PacketRepositoryImpl(private val dbManager: DatabaseProvider, private val
emptyMap()
} else {
withContext(dispatchers.io) {
val dao = dbManager.currentDb.value.packetDao()
ids.chunked(NodeInfoDao.MAX_BIND_PARAMS)
.flatMap { dao.getPacketsByPacketIds(it) }
.associateBy { it.packet.packetId }
dbManager.currentDb.value.packetDao().getPacketsByPacketIds(ids).associateBy { it.packet.packetId }
}
}

View file

@ -47,7 +47,6 @@ import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.repository.PlatformAnalytics
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.ServiceBroadcasts
import org.meshtastic.core.repository.UiPrefs
import org.meshtastic.proto.AdminMessage
import org.meshtastic.proto.Channel
import org.meshtastic.proto.Config
@ -69,7 +68,6 @@ class MeshActionHandlerImplTest {
private val dataHandler = mock<MeshDataHandler>(MockMode.autofill)
private val analytics = mock<PlatformAnalytics>(MockMode.autofill)
private val meshPrefs = mock<MeshPrefs>(MockMode.autofill)
private val uiPrefs = mock<UiPrefs>(MockMode.autofill)
private val databaseManager = mock<DatabaseManager>(MockMode.autofill)
private val notificationManager = mock<NotificationManager>(MockMode.autofill)
private val messageProcessor = mock<MeshMessageProcessor>(MockMode.autofill)
@ -102,7 +100,6 @@ class MeshActionHandlerImplTest {
dataHandler = lazy { dataHandler },
analytics = analytics,
meshPrefs = meshPrefs,
uiPrefs = uiPrefs,
databaseManager = databaseManager,
notificationManager = notificationManager,
messageProcessor = lazy { messageProcessor },
@ -359,7 +356,7 @@ class MeshActionHandlerImplTest {
@Test
fun handleRequestPosition_provideLocation_validPosition_usesGivenPosition() {
handler = createHandler(testScope)
every { uiPrefs.shouldProvideNodeLocation(MY_NODE_NUM) } returns MutableStateFlow(true)
every { meshPrefs.shouldProvideNodeLocation(MY_NODE_NUM) } returns MutableStateFlow(true)
val validPosition = Position(37.7749, -122.4194, 10)
handler.handleRequestPosition(REMOTE_NODE_NUM, validPosition, MY_NODE_NUM)
@ -370,7 +367,7 @@ class MeshActionHandlerImplTest {
@Test
fun handleRequestPosition_provideLocation_invalidPosition_fallsBackToNodeDB() {
handler = createHandler(testScope)
every { uiPrefs.shouldProvideNodeLocation(MY_NODE_NUM) } returns MutableStateFlow(true)
every { meshPrefs.shouldProvideNodeLocation(MY_NODE_NUM) } returns MutableStateFlow(true)
every { nodeManager.nodeDBbyNodeNum } returns emptyMap()
val invalidPosition = Position(0.0, 0.0, 0)
@ -383,7 +380,7 @@ class MeshActionHandlerImplTest {
@Test
fun handleRequestPosition_doNotProvide_sendsZeroPosition() {
handler = createHandler(testScope)
every { uiPrefs.shouldProvideNodeLocation(MY_NODE_NUM) } returns MutableStateFlow(false)
every { meshPrefs.shouldProvideNodeLocation(MY_NODE_NUM) } returns MutableStateFlow(false)
val validPosition = Position(37.7749, -122.4194, 10)
handler.handleRequestPosition(REMOTE_NODE_NUM, validPosition, MY_NODE_NUM)

View file

@ -20,7 +20,7 @@ import androidx.room3.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.runBlocking
import okio.ByteString.Companion.toByteString
import org.junit.After
import org.junit.Before
@ -59,7 +59,7 @@ class MigrationTest {
)
@Before
fun createDb(): Unit = runTest {
fun createDb(): Unit = runBlocking {
val context = ApplicationProvider.getApplicationContext<android.content.Context>()
database =
Room.inMemoryDatabaseBuilder<MeshtasticDatabase>(
@ -77,7 +77,7 @@ class MigrationTest {
}
@Test
fun testMigrateChannelsByPSK_duplicatePSK() = runTest {
fun testMigrateChannelsByPSK_duplicatePSK() = runBlocking {
// PSK \"AQ==\" is base64 for single byte 0x01
val pskBytes = byteArrayOf(0x01).toByteString()
@ -103,7 +103,7 @@ class MigrationTest {
}
@Test
fun testMigrateChannelsByPSK_reorder() = runTest {
fun testMigrateChannelsByPSK_reorder() = runBlocking {
val pskA = byteArrayOf(0x01).toByteString()
val pskB = byteArrayOf(0x02).toByteString()
@ -122,7 +122,7 @@ class MigrationTest {
}
@Test
fun testMigrateChannelsByPSK_disambiguateByName() = runTest {
fun testMigrateChannelsByPSK_disambiguateByName() = runBlocking {
val pskA = byteArrayOf(0x01).toByteString()
insertPacket(channel = 0, text = "Msg A1")
@ -141,7 +141,7 @@ class MigrationTest {
}
@Test
fun testMigrateChannelsByPSK_preferSameIndexIfStillAmbiguous() = runTest {
fun testMigrateChannelsByPSK_preferSameIndexIfStillAmbiguous() = runBlocking {
val pskA = byteArrayOf(0x01).toByteString()
insertPacket(channel = 0, text = "Msg A")

View file

@ -271,42 +271,6 @@ abstract class CommonPacketDaoTest {
assertFalse(excludingFiltered.any { it.packet.filtered })
}
@Test
fun testGetPacketsByPacketIdsChunked() = runTest {
// Regression test for SQLITE_MAX_VARIABLE_NUMBER (999) limit. Inserting >999 packets and
// looking them up by id must not throw; callers are expected to chunk, and each chunk
// must return the correct rows.
val totalPackets = 2000
val chunkSize = NodeInfoDao.MAX_BIND_PARAMS
val contactKey = "chunk-test"
val baseTime = nowMillis
val packetIds = (1..totalPackets).toList()
packetIds.forEach { id ->
packetDao.insert(
Packet(
uuid = 0L,
myNodeNum = myNodeNum,
port_num = PortNum.TEXT_MESSAGE_APP.value,
contact_key = contactKey,
received_time = baseTime + id,
read = false,
data =
DataPacket(
to = DataPacket.ID_BROADCAST,
bytes = "Chunk $id".encodeToByteArray().toByteString(),
dataType = PortNum.TEXT_MESSAGE_APP.value,
),
packetId = id,
),
)
}
val fetched = packetIds.chunked(chunkSize).flatMap { packetDao.getPacketsByPacketIds(it) }
assertEquals(totalPackets, fetched.size)
assertEquals(packetIds.toSet(), fetched.map { it.packet.packetId }.toSet())
}
companion object {
private const val SAMPLE_SIZE = 10
}

View file

@ -58,8 +58,6 @@ kotlin {
implementation(libs.androidx.test.runner)
}
}
commonTest.dependencies { implementation(projects.core.testing) }
}
}

View file

@ -0,0 +1,2 @@
-keep class org.meshtastic.core.model.DataPacket
-keep class org.meshtastic.core.model.DataPacket$CREATOR

View file

@ -1,56 +0,0 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.model
/**
* App-level MQTT proxy connection state, decoupled from the MQTT library's internal type.
*
* Modeled as a sealed class so disconnect / reconnect events can carry diagnostic context the user-facing reason for
* an unexpected disconnect, or the most recent reconnect attempt failure without requiring downstream consumers to
* depend on the MQTT library's exception types.
*/
sealed class MqttConnectionState {
/** The MQTT proxy has not been started (disabled or not yet initialized). */
data object Inactive : MqttConnectionState()
/** The MQTT client is actively connecting to the broker. */
data object Connecting : MqttConnectionState()
/** The MQTT client is connected and subscribed to topics. */
data object Connected : MqttConnectionState()
/**
* The MQTT client lost connection and is attempting to reconnect.
*
* @property attempt 1-based attempt counter for the current reconnect loop.
* @property lastError Localized message from the most recent reconnect failure, if any.
*/
data class Reconnecting(val attempt: Int = 0, val lastError: String? = null) : MqttConnectionState()
/**
* The MQTT client is not connected to the broker.
*
* @property reason Localized failure message for an unexpected disconnect, or `null` for the idle / initial /
* intentional-close case (use [Idle]).
*/
data class Disconnected(val reason: String? = null) : MqttConnectionState() {
companion object {
/** Singleton for the idle / no-reason disconnected state. */
val Idle: Disconnected = Disconnected(reason = null)
}
}
}

View file

@ -1,52 +0,0 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.model
/**
* UI-friendly outcome of a one-shot MQTT broker reachability probe.
*
* Mirrors the failure shapes of `org.meshtastic.mqtt.ProbeResult` but stays in the model module so feature/UI code can
* consume the result without depending on the MQTT library.
*/
sealed class MqttProbeStatus {
/** Probe is currently in flight. */
data object Probing : MqttProbeStatus()
/**
* Broker accepted the connection. [serverInfo] is a short human-readable summary of any CONNACK properties that are
* useful to surface to the user.
*/
data class Success(val serverInfo: String?) : MqttProbeStatus()
/** Broker rejected the connection (CONNACK with non-zero reason code). */
data class Rejected(val reasonCode: Int, val reason: String?, val serverReference: String?) : MqttProbeStatus()
/** DNS lookup failed. */
data class DnsFailure(val message: String?) : MqttProbeStatus()
/** TCP socket could not be opened. */
data class TcpFailure(val message: String?) : MqttProbeStatus()
/** TLS handshake failed. */
data class TlsFailure(val message: String?) : MqttProbeStatus()
/** Probe exceeded its timeout. */
data class Timeout(val timeoutMs: Long) : MqttProbeStatus()
/** Any other / unclassified failure. */
data class Other(val message: String?) : MqttProbeStatus()
}

View file

@ -18,11 +18,8 @@
package org.meshtastic.core.model.util
import org.meshtastic.proto.Channel
import org.meshtastic.proto.Config
import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.ModuleConfig
import org.meshtastic.proto.MyNodeInfo
import org.meshtastic.proto.Telemetry
/**
@ -51,24 +48,6 @@ fun MeshPacket.toOneLineString(): String {
return this.toString().replace(redactedFields.toRegex()) { "${it.groupValues[1]}=[REDACTED]" }.replace('\n', ' ')
}
fun Channel.toOneLineString(): String {
// Redact the channel preshared key (psk) from logs.
val redactedFields = """(psk)=[^,}]+"""
return this.toString().replace(redactedFields.toRegex()) { "${it.groupValues[1]}=[REDACTED]" }.replace('\n', ' ')
}
fun ModuleConfig.toOneLineString(): String {
// Redact MQTT credentials from logs.
val redactedFields = """(password|username)=[^,}]+"""
return this.toString().replace(redactedFields.toRegex()) { "${it.groupValues[1]}=[REDACTED]" }.replace('\n', ' ')
}
fun MyNodeInfo.toOneLineString(): String {
// Redact the hardware unique identifier from logs.
val redactedFields = """(device_id)=[^,}]+"""
return this.toString().replace(redactedFields.toRegex()) { "${it.groupValues[1]}=[REDACTED]" }.replace('\n', ' ')
}
fun Any.toPIIString() = if (!isDebug) {
"<PII?>"
} else {

View file

@ -32,7 +32,5 @@ kotlin {
implementation(libs.jetbrains.navigation3.ui)
implementation(libs.kermit)
}
commonTest.dependencies { implementation(projects.core.testing) }
}
}

View file

@ -40,7 +40,8 @@ kotlin {
implementation(projects.core.ble)
implementation(libs.okio)
api(libs.meshtastic.mqtt.client)
implementation(libs.kmqtt.client)
implementation(libs.kmqtt.common)
implementation(libs.kotlinx.serialization.json)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.content.negotiation)

View file

@ -108,10 +108,7 @@ class SerialRadioTransport(
"Uptime: ${uptime}ms, " +
"Packets RX: $packetsReceived ($bytesReceived bytes)"
}
// USB unplug / cable error is transient — the transport will reconnect when
// the device is replugged or the OS re-enumerates the port. Only an explicit
// close() (user disconnects) should signal a permanent disconnect.
onDeviceDisconnect(waitForStopped = false, isPermanent = false)
onDeviceDisconnect(false)
}
},
)

View file

@ -87,11 +87,6 @@ internal class SerialConnectionImpl(
port.open(usbDeviceConnection)
port.setParameters(115200, UsbSerialPort.DATABITS_8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE)
// Assert DTR/RTS so native USB-CDC firmware (RAK4631 / nRF52840) recognizes the host as
// present and starts its serial-side Meshtastic protocol. Empirically, omitting these
// signals causes the firmware to never respond to WAKE_BYTES, stalling the handshake at
// Stage 1. Bridge-chip boards (CH340, CP210x, FTDI) tolerate the assertion.
port.dtr = true
port.rts = true

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2026 Meshtastic LLC
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -14,13 +14,18 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.firmware
package org.meshtastic.core.network.repository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import org.meshtastic.core.common.di.ApplicationCoroutineScope
import android.annotation.SuppressLint
import java.security.cert.X509Certificate
import javax.net.ssl.X509TrustManager
internal class TestApplicationCoroutineScope(dispatcher: CoroutineDispatcher) :
ApplicationCoroutineScope,
CoroutineScope by CoroutineScope(SupervisorJob() + dispatcher)
@SuppressLint("CustomX509TrustManager", "TrustAllX509TrustManager")
@Suppress("EmptyFunctionBlock")
class TrustAllX509TrustManager : X509TrustManager {
override fun checkClientTrusted(chain: Array<X509Certificate>?, authType: String?) {}
override fun checkServerTrusted(chain: Array<X509Certificate>?, authType: String?) {}
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
}

View file

@ -54,7 +54,9 @@ class UsbRepository(
_serialDevices
.mapLatest { serialDevices ->
val serialProber = usbSerialProberLazy.value
buildMap { serialDevices.forEach { (k, v) -> serialProber.probeDevice(v)?.let { put(k, it) } } }
buildMap {
serialDevices.forEach { (k, v) -> serialProber.probeDevice(v)?.let { driver -> put(k, driver) } }
}
}
.stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, emptyMap())
@ -81,8 +83,6 @@ class UsbRepository(
processLifecycle.coroutineScope.launch(dispatchers.default) { refreshStateInternal() }
}
private suspend fun refreshStateInternal() = withContext(dispatchers.default) {
val devices = usbManagerLazy.value?.deviceList ?: emptyMap()
_serialDevices.emit(devices)
}
private suspend fun refreshStateInternal() =
withContext(dispatchers.default) { _serialDevices.emit(usbManagerLazy.value?.deviceList ?: emptyMap()) }
}

View file

@ -22,8 +22,9 @@ import co.touchlab.kermit.Logger
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope
@ -36,7 +37,6 @@ import kotlinx.coroutines.job
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import org.meshtastic.core.ble.BleConnection
import org.meshtastic.core.ble.BleConnectionFactory
@ -133,11 +133,7 @@ class BleRadioTransport(
@Volatile private var isFullyConnected = false
private var connectionJob: Job? = null
// Never give up while the user has this device selected. Higher layers (SharedRadioInterfaceService)
// own the explicit-disconnect lifecycle and will close() us when the user picks a different device or
// toggles the connection off; until then, retry forever with the policy's exponential-backoff cap (60 s).
private val reconnectPolicy = BleReconnectPolicy(maxFailures = Int.MAX_VALUE)
private val reconnectPolicy = BleReconnectPolicy()
private val heartbeatSender =
HeartbeatSender(
@ -400,14 +396,14 @@ class BleRadioTransport(
}
/** Closes the connection to the device. */
override suspend fun close() {
override fun close() {
Logger.i { "[$address] Disconnecting. ${formatSessionStats()}" }
connectionScope.cancel("close() called")
// GATT cleanup must run under NonCancellable so a cancelled caller cannot skip it,
// which would leak BluetoothGatt and trigger status 133 on the next reconnect.
// Using withContext (not runBlocking) keeps the caller's thread free — this is
// critical when close() is invoked from the main thread during process shutdown.
withContext(NonCancellable) {
// GATT cleanup must outlive scope cancellation — GlobalScope is intentional.
// SharedRadioInterfaceService cancels the scope immediately after close(), so a
// coroutine launched there may never run, leaking BluetoothGatt (causes GATT 133).
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch {
try {
withTimeoutOrNull(GATT_CLEANUP_TIMEOUT) { bleConnection.disconnect() }
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {

View file

@ -26,11 +26,10 @@ import kotlin.time.Duration.Companion.seconds
/**
* Encapsulates the BLE reconnection policy with exponential backoff.
*
* The policy tracks consecutive failures and decides whether to retry or signal a transient disconnect (DeviceSleep).
* When [maxFailures] is reached the [execute] loop invokes [execute]'s `onPermanentDisconnect` callback and returns;
* set [maxFailures] to [Int.MAX_VALUE] (as [BleRadioTransport] does) to disable the give-up path entirely.
* The policy tracks consecutive failures and decides whether to retry, signal a transient disconnect (DeviceSleep), or
* give up permanently.
*
* @param maxFailures maximum consecutive failures before giving up; use [Int.MAX_VALUE] to retry indefinitely
* @param maxFailures maximum consecutive failures before giving up permanently
* @param failureThreshold after this many consecutive failures, signal a transient disconnect
* @param settleDelay delay before each connection attempt to let the BLE stack settle
* @param minStableConnection minimum time a connection must stay up to be considered "stable"
@ -149,18 +148,7 @@ class BleReconnectPolicy(
companion object {
const val DEFAULT_MAX_FAILURES = 10
const val DEFAULT_FAILURE_THRESHOLD = 3
/**
* Delay applied before every connection attempt (including the first) so the BLE stack and the firmware-side
* GATT session have time to settle.
*
* Empirically validated against the meshtastic-client KMP SDK probes (Apr 2026): with a 1.5 s pause between
* disconnectreconnect cycles, 3/54/5 attempts failed mid-handshake (Stage1Draining timeouts) because the
* firmware had not yet released its GATT session from the previous cycle. With 5 s pause, success rate rose
* to 5/5 against a strong (-53 dBm) link. 3 s is a conservative compromise on Android, whose BLE stack is more
* mature than btleplug+CoreBluetooth, but the firmware-side cleanup constraint is the same.
*/
val DEFAULT_SETTLE_DELAY = 3.seconds
val DEFAULT_SETTLE_DELAY = 1.seconds
val DEFAULT_MIN_STABLE_CONNECTION = 5.seconds
internal val RECONNECT_BASE_DELAY = 5.seconds

View file

@ -144,7 +144,7 @@ class MockRadioTransport(
}
}
override suspend fun close() {
override fun close() {
Logger.i { "Closing the mock transport" }
}

View file

@ -30,7 +30,7 @@ class NopRadioTransport(val address: String) : RadioTransport {
// No-op
}
override suspend fun close() {
override fun close() {
// No-op
}
}

View file

@ -35,22 +35,20 @@ abstract class StreamTransport(protected val callback: RadioTransportCallback, p
private val codec =
StreamFrameCodec(onPacketReceived = { callback.handleFromRadio(it) }, logTag = "StreamTransport")
override suspend fun close() {
override fun close() {
Logger.d { "Closing stream for good" }
onDeviceDisconnect(waitForStopped = true, isPermanent = true)
onDeviceDisconnect(true)
}
/**
* Signals the transport callback that the device has disconnected and optionally waits for the transport to stop.
* Notify the transport callback that our device has gone away, but wait for it to come back.
*
* @param waitForStopped if true we should wait for the transport to finish - must be false if called from inside
* transport callbacks
* @param isPermanent true only when the user has explicitly disconnected (e.g. [close] was called). USB unplug, I/O
* errors, and similar conditions are transient the transport may recover when the device is replugged or the OS
* re-enumerates. Defaults to false so callbacks default to "may come back"; [close] passes true explicitly to
* signal a user-initiated terminal disconnect.
* @param isPermanent true if the device is definitely gone (e.g. USB unplugged), false if it may come back (e.g.
* TCP transient disconnect). Defaults to true for serial subclasses may override with false.
*/
protected open fun onDeviceDisconnect(waitForStopped: Boolean, isPermanent: Boolean = false) {
protected open fun onDeviceDisconnect(waitForStopped: Boolean, isPermanent: Boolean = true) {
callback.onDisconnect(isPermanent = isPermanent)
}

View file

@ -17,8 +17,6 @@
package org.meshtastic.core.network.repository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import org.meshtastic.mqtt.ConnectionState
import org.meshtastic.proto.MqttClientProxyMessage
/** Interface defining the MQTT interactions used for proxying messages to and from the mesh. */
@ -40,7 +38,4 @@ interface MQTTRepository {
* @param retained Whether the message should be retained by the broker.
*/
fun publish(topic: String, data: ByteArray, retained: Boolean)
/** Observable MQTT connection lifecycle state (DISCONNECTED → CONNECTING → CONNECTED → RECONNECTING). */
val connectionState: StateFlow<ConnectionState>
}

View file

@ -17,15 +17,22 @@
package org.meshtastic.core.network.repository
import co.touchlab.kermit.Logger
import io.github.davidepianca98.MQTTClient
import io.github.davidepianca98.mqtt.MQTTException
import io.github.davidepianca98.mqtt.MQTTVersion
import io.github.davidepianca98.mqtt.Subscription
import io.github.davidepianca98.mqtt.packets.Qos
import io.github.davidepianca98.mqtt.packets.mqttv5.ReasonCode
import io.github.davidepianca98.mqtt.packets.mqttv5.SubscriptionOptions
import io.github.davidepianca98.socket.IOException
import io.github.davidepianca98.socket.tls.TLSClientSettings
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.ProducerScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@ -37,19 +44,11 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonDecodingException
import okio.ByteString.Companion.toByteString
import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.safeCatching
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.model.MqttJsonPayload
import org.meshtastic.core.model.util.subscribeList
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.mqtt.ConnectionState
import org.meshtastic.mqtt.MqttClient
import org.meshtastic.mqtt.MqttEndpoint
import org.meshtastic.mqtt.MqttException
import org.meshtastic.mqtt.MqttMessage
import org.meshtastic.mqtt.QoS
import org.meshtastic.mqtt.packet.Subscription
import org.meshtastic.proto.MqttClientProxyMessage
import kotlin.concurrent.Volatile
@ -65,16 +64,12 @@ class MQTTRepositoryImpl(
private const val DEFAULT_TOPIC_LEVEL = "/2/e/"
private const val JSON_TOPIC_LEVEL = "/2/json/"
private const val DEFAULT_SERVER_ADDRESS = "mqtt.meshtastic.org"
private const val KEEPALIVE_SECONDS = 30
private const val INITIAL_RECONNECT_DELAY_MS = 1000L
private const val MAX_RECONNECT_DELAY_MS = 30_000L
private const val RECONNECT_BACKOFF_MULTIPLIER = 2
}
@Volatile private var client: MqttClient? = null
private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected.Idle)
override val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
@Volatile private var client: MQTTClient? = null
@OptIn(ExperimentalSerializationApi::class)
private val json = Json {
@ -82,17 +77,25 @@ class MQTTRepositoryImpl(
exceptionsWithDebugInfo = false
}
private val scope = CoroutineScope(dispatchers.default + SupervisorJob())
@Volatile private var clientJob: Job? = null
private val publishSemaphore = Semaphore(20)
@Suppress("TooGenericExceptionCaught")
override fun disconnect() {
Logger.i { "MQTT Disconnecting" }
val c = client
client = null
_connectionState.value = ConnectionState.Disconnected.Idle
scope.launch { safeCatching { c?.close() }.onFailure { e -> Logger.w(e) { "MQTT clean disconnect failed" } } }
client = null // Null first to prevent re-entrant disconnect
try {
c?.disconnect(ReasonCode.SUCCESS)
} catch (e: Exception) {
Logger.w(e) { "MQTT clean disconnect failed" }
}
clientJob?.cancel()
clientJob = null
}
@OptIn(ExperimentalSerializationApi::class)
@OptIn(ExperimentalUnsignedTypes::class)
override val proxyMessageFlow: Flow<MqttClientProxyMessage> = callbackFlow {
val ownerId = "MeshtasticAndroidMqttProxy-${nodeRepository.myId.value ?: "unknown"}"
val channelSet = radioConfigRepository.channelSetFlow.first()
@ -100,105 +103,108 @@ class MQTTRepositoryImpl(
val rootTopic = mqttConfig?.root?.ifEmpty { DEFAULT_TOPIC_ROOT } ?: DEFAULT_TOPIC_ROOT
val rawAddress = mqttConfig?.address ?: DEFAULT_SERVER_ADDRESS
val endpoint = resolveEndpoint(rawAddress, mqttConfig?.tls_enabled == true)
val (host, port) =
(mqttConfig?.address ?: DEFAULT_SERVER_ADDRESS).split(":", limit = 2).let {
it[0] to (it.getOrNull(1)?.toIntOrNull() ?: if (mqttConfig?.tls_enabled == true) 8883 else 1883)
}
val newClient =
MqttClient(ownerId) {
keepAliveSeconds = KEEPALIVE_SECONDS
autoReconnect = true
username = mqttConfig?.username
mqttConfig?.password?.let { password(it) }
}
MQTTClient(
mqttVersion = MQTTVersion.MQTT5,
address = host,
port = port,
tls = if (mqttConfig?.tls_enabled == true) TLSClientSettings() else null,
userName = mqttConfig?.username,
password = mqttConfig?.password?.encodeToByteArray()?.toUByteArray(),
clientId = ownerId,
publishReceived = { packet ->
val topic = packet.topicName
val payload = packet.payload?.toByteArray()
Logger.d { "MQTT received message on topic $topic (size: ${payload?.size ?: 0} bytes)" }
if (topic.contains("/json/")) {
try {
val jsonStr = payload?.decodeToString() ?: ""
// Validate JSON by parsing it
json.decodeFromString<MqttJsonPayload>(jsonStr)
Logger.d { "MQTT parsed JSON payload successfully" }
trySend(MqttClientProxyMessage(topic = topic, text = jsonStr, retained = packet.retain))
} catch (e: JsonDecodingException) {
@OptIn(ExperimentalSerializationApi::class)
Logger.e(e) { "Failed to parse MQTT JSON: ${e.shortMessage} (path: ${e.path})" }
} catch (e: SerializationException) {
Logger.e(e) { "Failed to parse MQTT JSON: ${e.message}" }
} catch (e: IllegalArgumentException) {
Logger.e(e) { "Failed to parse MQTT JSON: ${e.message}" }
}
} else {
trySend(
MqttClientProxyMessage(
topic = topic,
data_ = payload?.toByteString() ?: okio.ByteString.EMPTY,
retained = packet.retain,
),
)
}
},
)
client = newClient
val subscriptions: List<Subscription> = buildList {
channelSet.subscribeList.forEach { globalId ->
add(
Subscription(
"$rootTopic$DEFAULT_TOPIC_LEVEL$globalId/+",
maxQos = QoS.AT_LEAST_ONCE,
noLocal = true,
),
// Subscribe before starting the event loop. KMQTT's subscribe() calls send(),
// which queues the SUBSCRIBE packet in pendingSendMessages while connackReceived
// is false. Once the event loop receives CONNACK, it flushes the queue — so
// subscriptions are guaranteed to be sent immediately after the connection is
// established, with no timing races. This replaces a previous yield()-based
// approach that was unreliable on lightly loaded dispatchers.
val subscriptions = mutableListOf<Subscription>()
channelSet.subscribeList.forEach { globalId ->
subscriptions.add(
Subscription("$rootTopic$DEFAULT_TOPIC_LEVEL$globalId/+", SubscriptionOptions(Qos.AT_LEAST_ONCE)),
)
if (mqttConfig?.json_enabled == true) {
subscriptions.add(
Subscription("$rootTopic$JSON_TOPIC_LEVEL$globalId/+", SubscriptionOptions(Qos.AT_LEAST_ONCE)),
)
if (mqttConfig?.json_enabled == true) {
add(
Subscription(
"$rootTopic$JSON_TOPIC_LEVEL$globalId/+",
maxQos = QoS.AT_LEAST_ONCE,
noLocal = true,
),
)
}
}
add(Subscription("$rootTopic${DEFAULT_TOPIC_LEVEL}PKI/+", maxQos = QoS.AT_LEAST_ONCE, noLocal = true))
}
// Collect from the SharedFlow before connecting to avoid missing retained messages
// that arrive immediately after SUBSCRIBE.
launch { newClient.messages.collect { msg -> processMessage(msg) } }
// Forward the client's connection state to the repo-level StateFlow for UI observation.
launch { newClient.connectionState.collect { _connectionState.value = it } }
// Retry the initial connect with exponential backoff. Once established,
// autoReconnect handles subsequent drops and re-subscribes internally.
launch {
var reconnectDelay = INITIAL_RECONNECT_DELAY_MS
while (true) {
val result = safeCatching {
Logger.i { "MQTT Connecting to $endpoint" }
newClient.connect(endpoint)
if (subscriptions.isNotEmpty()) {
Logger.d { "MQTT subscribing to ${subscriptions.size} topics" }
newClient.subscribe(subscriptions)
}
Logger.i { "MQTT connected and subscribed" }
}
when {
result.isSuccess -> return@launch
result.exceptionOrNull() is MqttException.ConnectionRejected -> {
Logger.e(result.exceptionOrNull()) { "MQTT connection rejected (unrecoverable), stopping" }
close(result.exceptionOrNull()!!)
return@launch
}
else -> {
Logger.e(result.exceptionOrNull()) { "MQTT connect failed, retrying in ${reconnectDelay}ms" }
delay(reconnectDelay)
reconnectDelay =
(reconnectDelay * RECONNECT_BACKOFF_MULTIPLIER).coerceAtMost(MAX_RECONNECT_DELAY_MS)
}
}
}
}
subscriptions.add(Subscription("$rootTopic${DEFAULT_TOPIC_LEVEL}PKI/+", SubscriptionOptions(Qos.AT_LEAST_ONCE)))
if (subscriptions.isNotEmpty()) {
Logger.d { "MQTT subscribing to ${subscriptions.size} topics" }
newClient.subscribe(subscriptions)
}
clientJob =
scope.launch {
var reconnectDelay = INITIAL_RECONNECT_DELAY_MS
while (true) {
try {
Logger.i { "MQTT Starting client loop for $host:$port" }
newClient.runSuspend()
// runSuspend returned normally — broker closed connection cleanly.
// Reset backoff so the next reconnect starts with the minimum delay.
reconnectDelay = INITIAL_RECONNECT_DELAY_MS
Logger.w { "MQTT client loop ended normally, reconnecting in ${reconnectDelay}ms" }
} catch (e: MQTTException) {
Logger.e(e) { "MQTT Client loop error (MQTT), reconnecting in ${reconnectDelay}ms" }
} catch (e: IOException) {
Logger.e(e) { "MQTT Client loop error (IO), reconnecting in ${reconnectDelay}ms" }
} catch (e: CancellationException) {
Logger.i { "MQTT Client loop cancelled" }
throw e
}
delay(reconnectDelay)
reconnectDelay =
(reconnectDelay * RECONNECT_BACKOFF_MULTIPLIER).coerceAtMost(MAX_RECONNECT_DELAY_MS)
}
}
awaitClose { disconnect() }
}
@OptIn(ExperimentalSerializationApi::class)
private fun ProducerScope<MqttClientProxyMessage>.processMessage(msg: MqttMessage) {
val topic = msg.topic
val payload = msg.payload.toByteArray()
Logger.d { "MQTT received message on topic $topic (size: ${payload.size} bytes)" }
if (topic.contains("/json/")) {
try {
val jsonStr = payload.decodeToString()
json.decodeFromString<MqttJsonPayload>(jsonStr)
Logger.d { "MQTT parsed JSON payload successfully" }
trySend(MqttClientProxyMessage(topic = topic, text = jsonStr, retained = msg.retain))
} catch (e: JsonDecodingException) {
Logger.e(e) { "Failed to parse MQTT JSON: ${e.shortMessage} (path: ${e.path})" }
} catch (e: SerializationException) {
Logger.e(e) { "Failed to parse MQTT JSON: ${e.message}" }
} catch (e: IllegalArgumentException) {
Logger.e(e) { "Failed to parse MQTT JSON: ${e.message}" }
}
} else {
trySend(MqttClientProxyMessage(topic = topic, data_ = payload.toByteString(), retained = msg.retain))
}
}
@OptIn(ExperimentalUnsignedTypes::class)
override fun publish(topic: String, data: ByteArray, retained: Boolean) {
val currentClient = client
if (currentClient == null) {
@ -208,36 +214,18 @@ class MQTTRepositoryImpl(
Logger.d { "MQTT publishing message to topic $topic (size: ${data.size} bytes, retained: $retained)" }
scope.launch {
publishSemaphore.withPermit {
safeCatching {
@Suppress("TooGenericExceptionCaught")
try {
currentClient.publish(
MqttMessage(topic = topic, payload = data, qos = QoS.AT_LEAST_ONCE, retain = retained),
retain = retained,
qos = Qos.AT_LEAST_ONCE,
topic = topic,
payload = data.toUByteArray(),
)
} catch (e: Exception) {
Logger.w(e) { "MQTT publish to $topic failed" }
}
.onFailure { e -> Logger.w(e) { "MQTT publish to $topic failed" } }
}
}
}
}
/**
* Resolve a user-supplied broker address into an [MqttEndpoint].
*
* Address resolution rules:
* - If [rawAddress] already contains a URI scheme (`scheme://…`), parse it directly via [MqttEndpoint.parse] and
* respect whatever transport / port the user encoded.
* - Otherwise wrap it as a WebSocket endpoint (`ws[s]://host${WEBSOCKET_PATH}`) so the proxy works over CDNs and
* firewall-restricted networks where raw 1883/8883 may be blocked. The scheme is `wss` when [tlsEnabled] is `true`,
* `ws` otherwise.
*
* Extracted as a top-level function so [MQTTRepositoryImplTest] can exercise every branch without spinning up the full
* repository, and so `MqttManagerImpl` (in `:core:data`) can reuse the same parsing rules for the probe API. Visibility
* is `public` because Kotlin's `internal` is scoped per Gradle module.
*/
fun resolveEndpoint(rawAddress: String, tlsEnabled: Boolean): MqttEndpoint = if (rawAddress.contains("://")) {
MqttEndpoint.parse(rawAddress)
} else {
val scheme = if (tlsEnabled) "wss" else "ws"
MqttEndpoint.parse("$scheme://$rawAddress$WEBSOCKET_PATH")
}
private const val WEBSOCKET_PATH = "/mqtt"

View file

@ -22,7 +22,6 @@ import dev.mokkery.every
import dev.mokkery.matcher.any
import dev.mokkery.mock
import dev.mokkery.verify
import dev.mokkery.verify.VerifyMode
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
@ -96,10 +95,10 @@ class BleRadioTransportTest {
* [RadioInterfaceService.onDisconnect] must be called so the higher layers can react (e.g. start the device-sleep
* timeout in [MeshConnectionManagerImpl]).
*
* Virtual-time breakdown (DEFAULT_FAILURE_THRESHOLD = 3, DEFAULT_SETTLE_DELAY = 3 s): t = 3 000 ms iteration 1
* settle delay elapses, connectAndAwait throws, backoff 5 s starts t = 8 000 ms backoff ends t = 11 000 ms
* iteration 2 settle delay elapses, connectAndAwait throws, backoff 10 s starts t = 21 000 ms backoff ends t = 24
* 000 ms iteration 3 settle delay elapses, connectAndAwait throws onDisconnect called
* Virtual-time breakdown (DEFAULT_FAILURE_THRESHOLD = 3): t = 1 000 ms iteration 1 settle delay elapses,
* connectAndAwait throws, backoff 5 s starts t = 6 000 ms backoff ends t = 7 000 ms iteration 2 settle delay
* elapses, connectAndAwait throws, backoff 10 s starts t = 17 000 ms backoff ends t = 18 000 ms iteration 3
* settle delay elapses, connectAndAwait throws onDisconnect called
*/
@Test
fun `onDisconnect is called after DEFAULT_FAILURE_THRESHOLD consecutive failures`() = runTest {
@ -120,10 +119,10 @@ class BleRadioTransportTest {
)
bleTransport.start()
// Advance through exactly 3 failure iterations (≈24 001 ms virtual time).
// Advance through exactly 3 failure iterations (≈18 001 ms virtual time).
// The 4th iteration's backoff hasn't elapsed yet, so the coroutine is suspended
// and advanceTimeBy returns cleanly.
advanceTimeBy(24_001L)
advanceTimeBy(18_001L)
verify { service.onDisconnect(any(), any()) }
@ -132,17 +131,16 @@ class BleRadioTransportTest {
}
/**
* Reconnect policy must NEVER give up on its own. The transport is only ever instantiated for the user-selected
* device, and explicit-disconnect is owned by the service layer (close()). Even after a sustained failure storm
* well beyond the legacy [BleReconnectPolicy.DEFAULT_MAX_FAILURES] the transport must keep retrying and must
* never call `onDisconnect(isPermanent = true)` from the give-up path.
* After [BleReconnectPolicy.DEFAULT_MAX_FAILURES] (10) consecutive failures, the reconnect loop should stop and
* signal a permanent disconnect. This prevents infinite battery drain when the device is genuinely offline.
*
* Time budget for 15 failures with bonded device (no scan): each iteration 3 s settle + immediate throw +
* backoff. Backoffs cap at 60 s after failure 5: 5+10+20+40+60+60+60+60+60+60+60+60+60+60+60 = 735 s, plus 15×3 s
* settle = 45 s, total 780 s. Use 800_000 ms to cover variance.
* Time budget for 10 failures with bonded device (no scan): Each iteration = 1s settle + connectAndAwait throw +
* backoff Backoffs: 5s, 10s, 20s, 40s, 60s, 60s, 60s, 60s, 60s, (exit at failure 10 before backoff) Total 10×1s
* settle + 5+10+20+40+60+60+60+60+60 = 10 + 375 = 385s 385_000ms We use a generous 400_000ms to cover any timing
* variance.
*/
@Test
fun `reconnect loop never gives up - no permanent disconnect from policy`() = runTest {
fun `reconnect loop stops after DEFAULT_MAX_FAILURES with permanent disconnect`() = runTest {
val device = FakeBleDevice(address = address, name = "Test Device")
bluetoothRepository.bond(device)
@ -160,13 +158,11 @@ class BleRadioTransportTest {
)
bleTransport.start()
// Run well past where the legacy policy (maxFailures = 10) would have given up.
advanceTimeBy(800_001L)
// Advance enough time for all 10 failures to occur.
advanceTimeBy(400_001L)
// Transient disconnects (isPermanent = false) are expected once the failure threshold is hit;
// the policy must NEVER signal a permanent disconnect on its own. Only explicit close()
// (verified separately by the service layer) may emit isPermanent = true.
verify(mode = VerifyMode.not) { service.onDisconnect(isPermanent = true, errorMessage = any()) }
// Should have been called with isPermanent=true at least once (the final call).
verify { service.onDisconnect(isPermanent = true, errorMessage = any()) }
bleTransport.close()
}

View file

@ -18,82 +18,25 @@ package org.meshtastic.core.network.repository
import kotlinx.serialization.json.Json
import org.meshtastic.core.model.MqttJsonPayload
import org.meshtastic.mqtt.MqttEndpoint
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertTrue
class MQTTRepositoryImplTest {
// region resolveEndpoint — every behavioral branch of address parsing.
@Test
fun `bare host without scheme is wrapped as ws WebSocket on the standard port`() {
val endpoint = resolveEndpoint(rawAddress = "broker.example.com", tlsEnabled = false)
fun `test address parsing logic`() {
val address1 = "mqtt.example.com:1883"
val (host1, port1) = address1.split(":", limit = 2).let { it[0] to (it.getOrNull(1)?.toIntOrNull() ?: 1883) }
assertEquals("mqtt.example.com", host1)
assertEquals(1883, port1)
val ws = assertIs<MqttEndpoint.WebSocket>(endpoint)
assertEquals("ws://broker.example.com/mqtt", ws.url)
val address2 = "mqtt.example.com"
val (host2, port2) = address2.split(":", limit = 2).let { it[0] to (it.getOrNull(1)?.toIntOrNull() ?: 1883) }
assertEquals("mqtt.example.com", host2)
assertEquals(1883, port2)
}
@Test
fun `bare host with TLS enabled is upgraded to wss`() {
val endpoint = resolveEndpoint(rawAddress = "broker.example.com", tlsEnabled = true)
val ws = assertIs<MqttEndpoint.WebSocket>(endpoint)
assertEquals("wss://broker.example.com/mqtt", ws.url)
}
@Test
fun `host with explicit port is preserved when wrapped`() {
val endpoint = resolveEndpoint(rawAddress = "broker.example.com:9001", tlsEnabled = false)
val ws = assertIs<MqttEndpoint.WebSocket>(endpoint)
assertEquals("ws://broker.example.com:9001/mqtt", ws.url)
}
@Test
fun `address with ws scheme is parsed as-is and tls flag is ignored`() {
// tlsEnabled is intentionally true here — when the user supplies a full URL we
// must honor whatever scheme they provided, not silently upgrade it.
val endpoint = resolveEndpoint(rawAddress = "ws://broker.example.com:8080/custom-path", tlsEnabled = true)
val ws = assertIs<MqttEndpoint.WebSocket>(endpoint)
assertEquals("ws://broker.example.com:8080/custom-path", ws.url)
}
@Test
fun `address with wss scheme is parsed as-is`() {
val endpoint = resolveEndpoint(rawAddress = "wss://broker.example.com/secure-mqtt", tlsEnabled = false)
val ws = assertIs<MqttEndpoint.WebSocket>(endpoint)
assertEquals("wss://broker.example.com/secure-mqtt", ws.url)
}
@Test
fun `address with mqtt tcp scheme is parsed as Tcp endpoint`() {
val endpoint = resolveEndpoint(rawAddress = "mqtt://broker.example.com:1883", tlsEnabled = false)
val tcp = assertIs<MqttEndpoint.Tcp>(endpoint)
assertEquals("broker.example.com", tcp.host)
assertEquals(1883, tcp.port)
assertEquals(false, tcp.tls)
}
@Test
fun `address with mqtts tcp scheme is parsed as Tcp endpoint with tls true`() {
val endpoint = resolveEndpoint(rawAddress = "mqtts://broker.example.com:8883", tlsEnabled = false)
val tcp = assertIs<MqttEndpoint.Tcp>(endpoint)
assertEquals("broker.example.com", tcp.host)
assertEquals(8883, tcp.port)
assertEquals(true, tcp.tls)
}
// endregion
// region MqttJsonPayload — keep the existing JSON contract tests.
@Test
fun `test json payload parsing`() {
val jsonStr =
@ -129,6 +72,4 @@ class MQTTRepositoryImplTest {
assertTrue(jsonStr.contains("\"from\":12345678"))
assertTrue(jsonStr.contains("\"payload\":\"Hello World\""))
}
// endregion
}

View file

@ -74,15 +74,11 @@ open class TcpRadioTransport(
transport.start(address)
}
override suspend fun close() {
override fun close() {
Logger.d { "[$address] Closing TCP transport" }
closing = true
transport.stop()
// Do NOT emit onDisconnect(isPermanent = true) here. The explicit-disconnect signal is the
// service layer's responsibility (SharedRadioInterfaceService.stopTransportLocked); emitting
// it from close() caused a double-disconnect and prevented the auto-reconnect loop from
// owning its own lifecycle. The `closing` guard above suppresses the listener's transient
// disconnect during teardown.
callback.onDisconnect(isPermanent = true)
}
override fun keepAlive() {

View file

@ -129,10 +129,7 @@ private constructor(
// Ignore errors during port close
}
if (isActive) {
// Serial read loop ended unexpectedly (cable unplug, I/O error). Treat as
// transient — the user did not explicitly disconnect, and the port may come
// back when the device is replugged or the OS re-enumerates it.
onDeviceDisconnect(waitForStopped = true, isPermanent = false)
onDeviceDisconnect(true)
}
}
}
@ -157,7 +154,7 @@ private constructor(
serialPort = null
}
override suspend fun close() {
override fun close() {
Logger.d { "[$portName] Closing serial transport" }
readJob?.cancel()
readJob = null
@ -172,10 +169,8 @@ private constructor(
private const val READ_TIMEOUT_MS = 100
/**
* Creates and opens a [SerialTransport]. If the port cannot be opened, the transport signals a transient
* disconnect to the [callback] and returns the (non-connected) instance. The open failure is treated as
* non-permanent so higher-layer reconnect orchestration can retry (e.g. when the device is replugged or the
* user grants permission); only an explicit close should signal a permanent disconnect.
* Creates and opens a [SerialTransport]. If the port cannot be opened, the transport signals a permanent
* disconnect to the [callback] and returns the (non-connected) instance.
*/
fun open(
portName: String,
@ -188,7 +183,7 @@ private constructor(
if (!transport.startConnection()) {
val errorMessage = diagnoseOpenFailure(portName)
Logger.w { "[$portName] Serial port could not be opened; signalling disconnect. $errorMessage" }
callback.onDisconnect(isPermanent = false, errorMessage = errorMessage)
callback.onDisconnect(isPermanent = true, errorMessage = errorMessage)
}
return transport
}

View file

@ -19,31 +19,19 @@ package org.meshtastic.core.prefs
import kotlinx.atomicfu.AtomicRef
import kotlinx.collections.immutable.PersistentMap
/**
* Look up [key] in [cache]; if absent, construct a value via [build] and insert it atomically.
*
* [build] is wrapped in a [Lazy] before being published to [cache], so concurrent first-access of the same key never
* invokes [build] more than once only the winner of the CAS has its [Lazy] evaluated, and all readers share that same
* result. This matters when [build] eagerly launches a coroutine (e.g. `Flow.stateIn(scope, Eagerly, )`): the naive
* approach would leak the losing coroutine into a never-cancelled scope.
*/
@Suppress("ReturnCount")
internal inline fun <K, V> cachedFlow(
cache: AtomicRef<PersistentMap<K, Lazy<V>>>,
key: K,
crossinline build: () -> V,
): V {
cache.value[key]?.let {
return it.value
}
val newLazy = lazy(LazyThreadSafetyMode.SYNCHRONIZED) { build() }
while (true) {
val current = cache.value
current[key]?.let {
return it.value
}
if (cache.compareAndSet(current, current.put(key, newLazy))) {
return newLazy.value
internal inline fun <K, V> cachedFlow(cache: AtomicRef<PersistentMap<K, V>>, key: K, build: () -> V): V {
var resolved = cache.value[key]
if (resolved == null) {
val newValue = build()
while (resolved == null) {
val current = cache.value
val currentValue = current[key]
if (currentValue != null) {
resolved = currentValue
} else if (cache.compareAndSet(current, current.put(key, newValue))) {
resolved = newValue
}
}
}
return checkNotNull(resolved)
}

View file

@ -42,7 +42,7 @@ class MapConsentPrefsImpl(
) : MapConsentPrefs {
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)
private val consentFlows = atomic(persistentMapOf<Int?, Lazy<StateFlow<Boolean>>>())
private val consentFlows = atomic(persistentMapOf<Int?, StateFlow<Boolean>>())
override fun shouldReportLocation(nodeNum: Int?): StateFlow<Boolean> = cachedFlow(consentFlows, nodeNum) {
val key = booleanPreferencesKey(nodeNum.toString())

View file

@ -18,6 +18,7 @@ package org.meshtastic.core.prefs.mesh
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
@ -44,7 +45,8 @@ class MeshPrefsImpl(
) : MeshPrefs {
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)
private val storeForwardFlows = atomic(persistentMapOf<String?, Lazy<StateFlow<Int>>>())
private val locationFlows = atomic(persistentMapOf<Int?, StateFlow<Boolean>>())
private val storeForwardFlows = atomic(persistentMapOf<String?, StateFlow<Int>>())
override val deviceAddress: StateFlow<String?> =
dataStore.data
@ -63,6 +65,15 @@ class MeshPrefsImpl(
}
}
override fun shouldProvideNodeLocation(nodeNum: Int?): StateFlow<Boolean> = cachedFlow(locationFlows, nodeNum) {
val key = booleanPreferencesKey(provideLocationKey(nodeNum))
dataStore.data.map { it[key] ?: false }.stateIn(scope, SharingStarted.Eagerly, false)
}
override fun setShouldProvideNodeLocation(nodeNum: Int?, provide: Boolean) {
scope.launch { dataStore.edit { prefs -> prefs[booleanPreferencesKey(provideLocationKey(nodeNum))] = provide } }
}
override fun getStoreForwardLastRequest(address: String?): StateFlow<Int> = cachedFlow(storeForwardFlows, address) {
val key = intPreferencesKey(storeForwardKey(address))
dataStore.data.map { it[key] ?: 0 }.stateIn(scope, SharingStarted.Eagerly, 0)
@ -81,6 +92,8 @@ class MeshPrefsImpl(
}
}
private fun provideLocationKey(nodeNum: Int?) = "provide-location-$nodeNum"
private fun storeForwardKey(address: String?): String = "store-forward-last-request-${normalizeAddress(address)}"
companion object {

View file

@ -46,7 +46,7 @@ class UiPrefsImpl(
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)
// Maps nodeNum to a flow for the for the "provide-location-nodeNum" pref
private val provideNodeLocationFlows = atomic(persistentMapOf<Int, Lazy<StateFlow<Boolean>>>())
private val provideNodeLocationFlows = atomic(persistentMapOf<Int, StateFlow<Boolean>>())
override val appIntroCompleted: StateFlow<Boolean> =
dataStore.data.map { it[KEY_APP_INTRO_COMPLETED] ?: false }.stateIn(scope, SharingStarted.Eagerly, false)

View file

@ -0,0 +1,43 @@
# Core proto classes required for packet handling and serialization
# FromRadio and related message types (primary packet container)
-keep class org.meshtastic.proto.FromRadio
-keep class org.meshtastic.proto.Data
-keep class org.meshtastic.proto.MeshPacket
-keep class org.meshtastic.proto.LogRecord
# Message type payloads (handled in packet routing)
-keep class org.meshtastic.proto.AdminMessage
-keep class org.meshtastic.proto.StoreAndForward
-keep class org.meshtastic.proto.StoreForwardPlusPlus
-keep class org.meshtastic.proto.Routing
# User and Node information
-keep class org.meshtastic.proto.User
-keep class org.meshtastic.proto.NeighborInfo
-keep class org.meshtastic.proto.Neighbor
# Location and environment data
-keep class org.meshtastic.proto.Position
-keep class org.meshtastic.proto.Waypoint
-keep class org.meshtastic.proto.StatusMessage
# Telemetry data types
-keep class org.meshtastic.proto.Telemetry
-keep class org.meshtastic.proto.DeviceMetrics
-keep class org.meshtastic.proto.EnvironmentMetrics
-keep class org.meshtastic.proto.AirQualityMetrics
-keep class org.meshtastic.proto.PowerMetrics
-keep class org.meshtastic.proto.LocalStats
-keep class org.meshtastic.proto.HostMetrics
# Other data
-keep class org.meshtastic.proto.Paxcount
-keep class org.meshtastic.proto.DeviceMetadata
# Configuration classes
-keep class org.meshtastic.proto.ChannelSet
-keep class org.meshtastic.proto.LocalConfig
-keep class org.meshtastic.proto.Config
-keep class org.meshtastic.proto.ModuleConfig
-keep class org.meshtastic.proto.Channel
-keep class org.meshtastic.proto.ClientNotification

@ -1 +1 @@
Subproject commit 4d5b500df5af68a4f57d3e19705cc3bb1136358c
Subproject commit 940ac382a7d143040da5a880237f84c48ee31f2b

View file

@ -213,6 +213,10 @@ interface MeshPrefs {
fun setDeviceAddress(address: String?)
fun shouldProvideNodeLocation(nodeNum: Int?): StateFlow<Boolean>
fun setShouldProvideNodeLocation(nodeNum: Int?, provide: Boolean)
fun getStoreForwardLastRequest(address: String?): StateFlow<Int>
fun setStoreForwardLastRequest(address: String?, timestamp: Int)

View file

@ -16,16 +16,10 @@
*/
package org.meshtastic.core.repository
import kotlinx.coroutines.flow.StateFlow
import org.meshtastic.core.model.MqttConnectionState
import org.meshtastic.core.model.MqttProbeStatus
import org.meshtastic.proto.MqttClientProxyMessage
/** Interface for managing MQTT proxy communication. */
interface MqttManager {
/** Observable MQTT proxy connection state for UI consumption. */
val mqttConnectionState: StateFlow<MqttConnectionState>
/** Starts the MQTT proxy with the given settings. */
fun startProxy(enabled: Boolean, proxyToClientEnabled: Boolean)
@ -34,15 +28,4 @@ interface MqttManager {
/** Handles an MQTT proxy message from the radio. */
fun handleMqttProxyMessage(message: MqttClientProxyMessage)
/**
* Probe an MQTT broker to verify connectivity and credentials without joining the proxy lifecycle. Intended for UI
* "Test Connection" affordances.
*
* @param address Raw broker address as the user would type it (host, host:port, or full URL).
* @param tlsEnabled `true` to upgrade bare addresses to `wss://` (ignored when [address] already has a scheme).
* @param username Optional MQTT username.
* @param password Optional MQTT password.
*/
suspend fun probe(address: String, tlsEnabled: Boolean, username: String?, password: String?): MqttProbeStatus
}

View file

@ -17,7 +17,6 @@
package org.meshtastic.core.repository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import org.meshtastic.core.model.ConnectionState
@ -69,26 +68,12 @@ interface RadioInterfaceService : RadioTransportCallback {
/** Whether we are currently using a mock transport. */
fun isMockTransport(): Boolean
/**
* Flow of raw data received from the radio.
*
* Emissions preserve the order in which bytes arrived from the hardware this is required because the firmware
* handshake (initial config packet ordering) depends on strict FIFO delivery. Implementations MUST guarantee
* ordering; do not swap in a [SharedFlow] without preserving order.
*/
val receivedData: Flow<ByteArray>
/** Flow of raw data received from the radio. */
val receivedData: SharedFlow<ByteArray>
/** Flow of radio activity events. */
val meshActivity: SharedFlow<MeshActivity>
/**
* Drains any bytes currently buffered in [receivedData] without emitting them to collectors.
*
* Callers invoke this before attaching a fresh collector after a stop/start cycle so stale bytes buffered while no
* collector was attached do not get replayed ahead of the next session's handshake.
*/
fun resetReceivedBuffer()
/** Sends a raw byte array to the radio. */
fun sendToRadio(bytes: ByteArray)

View file

@ -16,11 +16,13 @@
*/
package org.meshtastic.core.repository
import okio.Closeable
/**
* Interface for hardware transports (BLE, Serial, TCP, etc.) that handles raw byte communication. This is the
* KMP-compatible replacement for the legacy Android-specific IRadioInterface.
*/
interface RadioTransport {
interface RadioTransport : Closeable {
/** Sends a raw byte array to the radio hardware. */
fun handleSendToRadio(p: ByteArray)
@ -37,13 +39,4 @@ interface RadioTransport {
* function can be implemented by transports to see if we are really connected.
*/
fun keepAlive() {}
/**
* Closes the connection to the device.
*
* Implementations that perform potentially-blocking teardown (e.g. BLE GATT disconnect) MUST run that work inside
* `withContext(NonCancellable)` so a cancelled caller cannot skip cleanup, leaving the underlying resource leaked.
* Callers must invoke this from a coroutine it must never be called from a blocking context (no `runBlocking`).
*/
suspend fun close()
}

View file

@ -16,14 +16,13 @@
*/
package org.meshtastic.core.repository
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertTrue
class RadioTransportTest {
@Test
fun `RadioTransport can be implemented`() = runTest {
fun `RadioTransport can be implemented`() {
var sentData: ByteArray? = null
var closed = false
var keepAliveCalled = false
@ -38,7 +37,7 @@ class RadioTransportTest {
keepAliveCalled = true
}
override suspend fun close() {
override fun close() {
closed = true
}
}

View file

@ -154,7 +154,6 @@
<string name="messages">الرسائل</string>
<string name="lora_config">إعدادات لورا</string>
<string name="region_frequency_plan">الجهة</string>
<string name="mqtt_status_disconnected">انقطع الاتصال</string>
<string name="timeout">استغرق وقت طويل</string>
<string name="distance">المسافة</string>
<string name="bottom_nav_settings">الإعدادات</string>
@ -174,5 +173,4 @@
<!-- Message Filter -->
<string name="bluetooth_permission">إعدادات بلوتوث</string>
<!-- Network Map Layers -->
<string name="filter_icon">عربي</string>
</resources>

View file

@ -167,8 +167,6 @@
<string name="play">Граць</string>
<string name="lora_config">LoRa</string>
<string name="region_frequency_plan">Рэгіён</string>
<string name="mqtt_status_disconnected">Адлучана</string>
<string name="mqtt_status_connected">Злучаны</string>
<string name="username">Імя карыстальніка</string>
<string name="password">Пароль</string>
<string name="enabled">Уключана</string>
@ -221,6 +219,4 @@
<string name="tak_team_red">Чырвоны</string>
<string name="tak_team_blue">Сіні</string>
<string name="tak_team_green">Зялёны</string>
<string name="desktop_notification_title">Meshtastic</string>
<string name="filter_icon">Фільтраваць</string>
</resources>

View file

@ -201,15 +201,10 @@
<string name="reset_to_defaults">Възстановяване на настройките по подразбиране</string>
<string name="apply">Приложи</string>
<string name="theme">Тема</string>
<string name="contrast">Контраст</string>
<string name="theme_light">Светла</string>
<string name="theme_dark">Тъмна</string>
<string name="theme_system">По подразбиране на системата</string>
<string name="choose_theme">Избор на тема</string>
<string name="choose_contrast">Ниво на контраста</string>
<string name="contrast_standard">Стандартен</string>
<string name="contrast_medium">Среден</string>
<string name="contrast_high">Висок</string>
<string name="provide_location_to_mesh">Изпращане на местоположение в мрежата</string>
<string name="use_homoglyph_characters_encoding">Компактно кодиране за Кирилица</string>
<plurals name="delete_messages">
@ -306,8 +301,6 @@
<string name="battery">Батерия</string>
<string name="channel_utilization">Използване на канала</string>
<string name="air_utilization">Използване на ефира</string>
<string name="device_metrics_percent_value">%1$s: %2$s%%</string>
<string name="device_metrics_voltage_value">%1$s: %2$s V</string>
<string name="device_metrics_numeric_value">%1$s</string>
<string name="device_metrics_label_value">%1$s: %2$s</string>
<string name="logs">записа</string>
@ -324,9 +317,12 @@
<string name="encryption_error_text">Публичният ключ не съвпада със записания ключ. Можете да премахнете възела и да го оставите да обмени ключове отново, но това може да показва по-сериозен проблем със сигурността. Свържете се с потребителя чрез друг надежден канал, за да определите дали промяната на ключа се дължи на фабрично нулиране или друго умишлено действие.</string>
<string name="meshtastic_new_nodes_notifications">Известия за нови възли</string>
<string name="snr">SNR</string>
<string name="snr_definition">Съотношение сигнал/шум, мярка, използвана в комуникациите за количествено определяне на нивото на желания сигнал спрямо нивото на фоновия шум. В Meshtastic и други безжични системи, по-високото съотношение сигнал/шум показва по-ясен сигнал, който може да подобри надеждността и качеството на предаване на данни.</string>
<string name="rssi">RSSI</string>
<string name="rssi_definition">Индикатор за силата на получения сигнал - измерване, използвано за определяне на нивото на получения сигнал, приемано от антената. По-високата стойност на RSSI обикновено показва по-силна и по-стабилна връзка.</string>
<string name="iaq_definition">(Качество на въздуха в помещенията) относителна скала за IAQ, стойностите са измерени с Bosch BME680. Диапазон на стойностите 0500.</string>
<string name="device_metrics_log">Метрики на устройството</string>
<string name="node_map">Карта на възела</string>
<string name="position_log">Позиция</string>
<string name="last_position_update">Последна актуализация на позицията</string>
<string name="env_metrics_log">Показатели на околната среда</string>
@ -366,6 +362,7 @@
<string name="one_month">1М</string>
<string name="max">Макс</string>
<string name="min">Мин</string>
<string name="avg">Ср</string>
<string name="expand_chart">Разгъване на диаграмата</string>
<string name="collapse_chart">Свиване на диаграмата</string>
<string name="unknown_age">Неизвестна възраст</string>
@ -486,17 +483,6 @@
<string name="frequency_slot">Честотен слот</string>
<string name="ignore_mqtt">Игнориране на MQTT</string>
<string name="mqtt_config">Конфигуриране на MQTT</string>
<string name="mqtt_status_inactive">Неактивен</string>
<string name="mqtt_status_disconnected">Прекъсната връзка</string>
<string name="mqtt_status_connecting">Свързване…</string>
<string name="mqtt_status_connected">Свързано</string>
<string name="mqtt_status_reconnecting">Повторно свързване…</string>
<string name="mqtt_status_reconnecting_with_attempt">Повторно свързване (опит %1$d) — %2$s</string>
<string name="mqtt_test_connection">Тестване на връзката</string>
<string name="mqtt_probe_success">Достъпен. Брокерът е приел идентификационните данни.</string>
<string name="mqtt_probe_success_with_info">Достъпен (%1$s)</string>
<string name="mqtt_probe_dns_failure">Хостът не е намерен</string>
<string name="mqtt_probe_other_failure">Връзката е неуспешна</string>
<string name="mqtt_enabled">MQTT е активиран</string>
<string name="address">Адрес</string>
<string name="username">Потребителско име</string>
@ -974,9 +960,4 @@
<string name="wifi_provision_ssid_placeholder">Въведете или изберете мрежа</string>
<string name="wifi_provision_status_applied">WiFi е конфигуриран успешно!</string>
<string name="wifi_provision_status_failed">Прилагането на конфигурацията за WiFi не е успешно</string>
<string name="desktop_tray_quit">Изход</string>
<string name="desktop_notification_title">Meshtastic</string>
<string name="filter_icon">Филтър</string>
<string name="action_select_device">Изберете устройство</string>
<string name="action_select_network">Изберете мрежа</string>
</resources>

View file

@ -182,7 +182,6 @@
<string name="mute_always">Sempre</string>
<string name="traceroute_log">Traçar ruta</string>
<string name="region_frequency_plan">Regió</string>
<string name="mqtt_status_disconnected">Desconnectat</string>
<string name="timeout">Temps esgotat</string>
<string name="distance">Distància</string>
<string name="meshtastic">Meshtastic</string>
@ -200,6 +199,4 @@
<!-- Compass -->
<!-- Message Filter -->
<!-- Network Map Layers -->
<string name="desktop_notification_title">Meshtastic</string>
<string name="filter_icon">Filtre</string>
</resources>

View file

@ -331,9 +331,12 @@
<string name="userinfo">Informace o uživateli</string>
<string name="meshtastic_new_nodes_notifications">Oznámení o nových uzlech</string>
<string name="snr">SNR</string>
<string name="snr_definition">Poměr signálu k šumu (SNR) je veličina používaná k vyjádření poměru mezi úrovní požadovaného signálu a úrovní šumu na pozadí. V Meshtastic a dalších bezdrátových systémech vyšší hodnota SNR značí čistší signál, což může zvýšit spolehlivost a kvalitu přenosu dat.</string>
<string name="rssi">RSSI</string>
<string name="rssi_definition">Indikátor síly přijímaného signálu, měření, které se používá k určení hladiny výkonu přijímané anténou. Vyšší hodnota RSSI obvykle znamená silnější a stabilnější spojení.</string>
<string name="iaq_definition">(Vnitřní kvalita ovzduší) relativní hodnota IAQ měřená Bosch BME680. Hodnota rozsahu 0500.</string>
<string name="device_metrics_log">Metriky zařízení</string>
<string name="node_map">Mapa uzlu</string>
<string name="position_log">Pozice</string>
<string name="last_position_update">Poslední aktualizace pozice</string>
<string name="env_metrics_log">Metriky prostředí</string>
@ -522,8 +525,6 @@
<string name="ignore_mqtt">Ignorovat MQTT</string>
<string name="ok_to_mqtt">OK do MQTT</string>
<string name="mqtt_config">Nastavení MQTT</string>
<string name="mqtt_status_disconnected">Odpojeno</string>
<string name="mqtt_status_connected">Připojeno</string>
<string name="mqtt_enabled">MQTT povoleno</string>
<string name="address">Adresa</string>
<string name="username">Uživatelské jméno</string>
@ -967,6 +968,4 @@
<string name="note">Poznámka</string>
<string name="connect">Připojit</string>
<string name="done">Hotovo</string>
<string name="desktop_notification_title">Meshtastic</string>
<string name="filter_icon">Filtr</string>
</resources>

View file

@ -253,13 +253,10 @@
<string name="reset_to_defaults">Auf Standardeinstellungen zurücksetzen</string>
<string name="apply">Anwenden</string>
<string name="theme">Design</string>
<string name="contrast">Kontrast</string>
<string name="theme_light">Hell</string>
<string name="theme_dark">Dunkel</string>
<string name="theme_system">System</string>
<string name="choose_theme">Design auswählen</string>
<string name="choose_contrast">Kontrast</string>
<string name="contrast_standard">Standard</string>
<string name="contrast_medium">Medium Fast</string>
<string name="contrast_high">Hoch</string>
<string name="provide_location_to_mesh">Standort zum Mesh angeben</string>
@ -359,8 +356,6 @@
<string name="battery">Akku</string>
<string name="channel_utilization">Kanalauslastung</string>
<string name="air_utilization">Sendezeit</string>
<string name="device_metrics_percent_value">%1$s: %2$s%%</string>
<string name="device_metrics_voltage_value">%1$s: %2$s V</string>
<string name="device_metrics_numeric_value">%1$s</string>
<string name="device_metrics_label_value">%1$s: %2$s</string>
<string name="temperature">Temperatur</string>
@ -383,9 +378,12 @@
<string name="userinfo">Benutzerinfo</string>
<string name="meshtastic_new_nodes_notifications">Benachrichtigung neue Knoten</string>
<string name="snr">SNR</string>
<string name="snr_definition">Signal-Rausch-Verhältnis, ein in der Kommunikation verwendetes Maß, um den Pegel eines gewünschten Signals im Verhältnis zum Pegel des Hintergrundrauschens zu quantifizieren. Bei Meshtastic und anderen drahtlosen Systemen weist ein höheres SNR auf ein klareres Signal hin, das die Zuverlässigkeit und Qualität der Datenübertragung verbessern kann.</string>
<string name="rssi">RSSI</string>
<string name="rssi_definition">Indikator für die empfangene Signalstärke, eine Messung zur Bestimmung der von der Antenne empfangenen Leistungsstärke. Ein höherer RSSI-Wert weist im Allgemeinen auf eine stärkere und stabilere Verbindung hin.</string>
<string name="iaq_definition">(Innenluftqualität) relativer IAQ-Wert gemessen von Bosch BME680.</string>
<string name="device_metrics_log">Gerätedaten</string>
<string name="node_map">Standortkarte Knoten</string>
<string name="position_log">Standort</string>
<string name="last_position_update">Letzte Standortaktualisierung</string>
<string name="env_metrics_log">Umweltdaten</string>
@ -432,6 +430,7 @@
<string name="one_month">1 Monat</string>
<string name="max">Maximal</string>
<string name="min">Minimum</string>
<string name="avg">Durchschnitt</string>
<string name="expand_chart">Diagramm einblenden</string>
<string name="collapse_chart">Diagramm ausblenden</string>
<string name="unknown_age">Alter unbekannt</string>
@ -583,9 +582,6 @@
<string name="output_duration_milliseconds">Ausgabedauer (GPIO)</string>
<string name="nag_timeout_seconds">Nervige Verzögerung (Sekunden)</string>
<string name="ringtone">Klingelton</string>
<string name="ringtone_imported">Importierter Klingelton</string>
<string name="ringtone_file_empty">Datei ist leer</string>
<string name="ringtone_import_error">Fehler beim Importieren: %1$s</string>
<string name="play">Wiedergabe</string>
<string name="use_i2s_as_buzzer">I2S als Buzzer verwenden</string>
<string name="lora_config">LoRa</string>
@ -609,23 +605,6 @@
<string name="ignore_mqtt">MQTT ignorieren</string>
<string name="ok_to_mqtt">OK für MQTT</string>
<string name="mqtt_config">MQTT Einstellungen</string>
<string name="mqtt_status_inactive">Inaktiv</string>
<string name="mqtt_status_disconnected">Verbindung getrennt</string>
<string name="mqtt_status_disconnected_with_reason">Verbindung getrennt - %1$s</string>
<string name="mqtt_status_connecting">Wird verbunden</string>
<string name="mqtt_status_connected">Verbunden</string>
<string name="mqtt_status_reconnecting">Erneut verbinden</string>
<string name="mqtt_status_reconnecting_with_attempt">Erneut verbinden (Versuch %1$d) - %2$s</string>
<string name="mqtt_test_connection">Verbindung testen</string>
<string name="mqtt_probe_running">Broker prüfen.</string>
<string name="mqtt_probe_success">Erreichbar. Broker akzeptierte Anmeldedaten.</string>
<string name="mqtt_probe_success_with_info">Erreichbar (%1$s)</string>
<string name="mqtt_probe_rejected">Broker abgelehnt: %1$s</string>
<string name="mqtt_probe_dns_failure">Host nicht gefunden</string>
<string name="mqtt_probe_tcp_failure">Broker (TCP) nicht erreichbar</string>
<string name="mqtt_probe_tls_failure">TLS Handshake fehlgeschlagen</string>
<string name="mqtt_probe_timeout">Zeitüberschreitung nach %1$d ms</string>
<string name="mqtt_probe_other_failure">Verbindung fehlgeschlagen</string>
<string name="mqtt_enabled">MQTT aktiviert</string>
<string name="address">Adresse</string>
<string name="username">Benutzername</string>
@ -741,7 +720,6 @@
<string name="rainfall_24h">Regen (24 Std.)</string>
<string name="weight">Gewicht</string>
<string name="radiation">Strahlung</string>
<string name="one_wire_temperature">1-Wire Temperature</string>
<string name="store_forward_config"><![CDATA[Speichern & Weiterleiten Einstellungen]]></string>
<string name="indoor_air_quality_iaq">Luftqualität im Innenbereich (IAQ)</string>
<string name="url">URL</string>
@ -1221,21 +1199,4 @@
<string name="wifi_provision_ssid_placeholder">Netzwerk eingeben oder auswählen</string>
<string name="wifi_provision_status_applied">WLAN erfolgreich konfiguriert!</string>
<string name="wifi_provision_status_failed">WLAN Konfiguration konnte nicht angewendet werden</string>
<string name="desktop_tray_tooltip">Meshtastic Desktop</string>
<string name="desktop_tray_show">Meshtastic anzeigen</string>
<string name="desktop_tray_quit">Beenden</string>
<string name="desktop_notification_title">Meshtastic</string>
<string name="export_tak_data_package">TAK Datenpaket exportieren</string>
<string name="clear_time_zone">Zeitzone löschen</string>
<string name="filter_icon">Filter</string>
<string name="remove_filter">Filter entfernen</string>
<string name="show_iaq_legend">Legende für Luftqualität anzeigen</string>
<string name="action_show_message_status">Nachrichtenstatus anzeigen</string>
<string name="action_send_reply">Antwort senden</string>
<string name="action_copy_message">Nachricht kopieren</string>
<string name="action_select_message">Nachricht auswählen</string>
<string name="action_delete_message">Nachricht löschen</string>
<string name="action_react_with_emoji">Mit Emoji reagieren</string>
<string name="action_select_device">Gerät auswählen</string>
<string name="action_select_network">Wählen Sie ein Netzwerk</string>
</resources>

View file

@ -164,7 +164,6 @@
<string name="messages">Μηνύματα</string>
<string name="lora_config">LoRa</string>
<string name="region_frequency_plan">Περιφέρεια</string>
<string name="mqtt_status_disconnected">Αποσυνδεδεμένο</string>
<string name="address">Διεύθυνση</string>
<string name="username">Όνομα χρήστη</string>
<string name="password">Κωδικός πρόσβασης</string>
@ -201,5 +200,4 @@
<string name="tak_team_red">Κόκκινο</string>
<string name="tak_team_blue">Μπλε</string>
<string name="tak_team_green">Πράσινο</string>
<string name="filter_icon">Φίλτρο</string>
</resources>

View file

@ -300,10 +300,13 @@
<string name="encryption_error">Clave pública no coincide</string>
<string name="meshtastic_new_nodes_notifications">Notificaciones de nuevo nodo</string>
<string name="snr">SNR</string>
<string name="snr_definition">SNR: Ratio de señal a ruido, una medida utilizada en las comunicaciones para cuantificar el nivel de una señal deseada respecto al nivel del ruido de fondo. En Meshtastic y otros sistemas inalámbricos, un mayor SNR indica una señal más clara que puede mejorar la fiabilidad y la calidad de la transmisión de datos.</string>
<string name="rssi">RSSI</string>
<string name="rssi_definition">Indicador de Fuerza de Señal Recibida (RSSI en inglés), una medida utilizada para determinar el nivel de potencia que está siendo recibido por la antena. Un valor de RSSI más alto generalmente indica una conexión más fuerte y estable.</string>
<string name="iaq_definition">(Calidad de Aire interior) escala relativa del valor IAQ como mediciones del sensor Bosch BME680.
Rango de Valores 0 - 500.</string>
<string name="device_metrics_log">Métricas de Dispositivo</string>
<string name="node_map">Mapa de Nodos</string>
<string name="position_log">Posición</string>
<string name="last_position_update">Última actualización</string>
<string name="env_metrics_log">Métricas de Entorno</string>
@ -489,8 +492,6 @@ Rango de Valores 0 - 500.</string>
<string name="ignore_mqtt">Ignorar Paquetes MQTT</string>
<string name="ok_to_mqtt">Permitir MQTT</string>
<string name="mqtt_config">Configuración MQTT</string>
<string name="mqtt_status_disconnected">Desconectado</string>
<string name="mqtt_status_connected">Conectado</string>
<string name="mqtt_enabled">Activar el MQTT</string>
<string name="address">Dirección del Servidor MQTT</string>
<string name="username">Usuario</string>
@ -835,6 +836,4 @@ Estos datos de ubicación pueden ser utilizados para fines como aparecer en un m
<string name="tak_team_green">Verde</string>
<string name="connect">Conectar</string>
<string name="done">Hecho</string>
<string name="desktop_notification_title">Meshtastic</string>
<string name="filter_icon">Filtro</string>
</resources>

View file

@ -383,9 +383,12 @@
<string name="userinfo">Kasutaja teave</string>
<string name="meshtastic_new_nodes_notifications">Uue sõlme teade</string>
<string name="snr">SNR</string>
<string name="snr_definition">Signaali ja müra suhe (SNR) on mõõdik, mida kasutatakse soovitud signaali taseme ja taustamüra taseme vahelise suhte määramisel. Meshtastic ja teistes traadita süsteemides näitab kõrgem signaali ja müra suhe selgemat signaali, mis võib parandada andmeedastuse usaldusväärsust ja kvaliteeti.</string>
<string name="rssi">RSSI</string>
<string name="rssi_definition">Vastuvõetud signaali tugevuse indikaator (RSSI), mõõt mida kasutatakse antenni poolt vastuvõetava võimsustaseme määramiseks. Kõrgem RSSI väärtus näitab üldiselt tugevamat ja stabiilsemat ühendust.</string>
<string name="iaq_definition">Siseõhu kvaliteet (IAQ) on suhtelise skaala väärtus, näiteks mõõtes Bosch BME680 abil. Väärtuste vahemik 0500.</string>
<string name="device_metrics_log">Seadme mõõdikud</string>
<string name="node_map">Sõlmede kaart</string>
<string name="position_log">Asukoht</string>
<string name="last_position_update">Viimase asukoha värskendus</string>
<string name="env_metrics_log">Keskkonnamõõdikud</string>
@ -432,6 +435,7 @@
<string name="one_month">1k</string>
<string name="max">Maksimaalselt</string>
<string name="min">Min</string>
<string name="avg">Keskm</string>
<string name="expand_chart">Laienda diagrammi</string>
<string name="collapse_chart">Ahenda diagrammi</string>
<string name="unknown_age">Tundmatu vanus</string>
@ -609,23 +613,6 @@
<string name="ignore_mqtt">Keela MQTT</string>
<string name="ok_to_mqtt">Ok MQTTi</string>
<string name="mqtt_config">MQTT sätted</string>
<string name="mqtt_status_inactive">Mitteaktiivne</string>
<string name="mqtt_status_disconnected">Ühendus katkenud</string>
<string name="mqtt_status_disconnected_with_reason">Ühendus katkenud — %1$s</string>
<string name="mqtt_status_connecting">Ühendan…</string>
<string name="mqtt_status_connected">Ühendatud</string>
<string name="mqtt_status_reconnecting">Taas ühendan…</string>
<string name="mqtt_status_reconnecting_with_attempt">Ühendan uuesti (katse %1$d) — %2$s</string>
<string name="mqtt_test_connection">Test ühendus</string>
<string name="mqtt_probe_running">Kontrollin vahendajat…</string>
<string name="mqtt_probe_success">Ühendus õnnestus. Vahendaja aktsepteeris kasutajateave.</string>
<string name="mqtt_probe_success_with_info">Kättesaadav (%1$s)</string>
<string name="mqtt_probe_rejected">Vahendaja lükkas tagasi: %1$s</string>
<string name="mqtt_probe_dns_failure">Hosti ei leitud</string>
<string name="mqtt_probe_tcp_failure">Vahendajaga ei saa ühendust (TCP)</string>
<string name="mqtt_probe_tls_failure">TLS ühendus ebaõnnestus</string>
<string name="mqtt_probe_timeout">Ajaline katkestus peale %1$d ms</string>
<string name="mqtt_probe_other_failure">Ühendus ebaõnnestus</string>
<string name="mqtt_enabled">MQTT lubatud</string>
<string name="address">Aadress</string>
<string name="username">Kasutajatunnus</string>
@ -1221,21 +1208,4 @@
<string name="wifi_provision_ssid_placeholder">Sisestage või valige võrk</string>
<string name="wifi_provision_status_applied">WiFi edukalt seadistatud!</string>
<string name="wifi_provision_status_failed">WiFi sätete rakendamine ebaõnnestus</string>
<string name="desktop_tray_tooltip">Meshtastic töölaud</string>
<string name="desktop_tray_show">Näita Meshtastic</string>
<string name="desktop_tray_quit">Sule</string>
<string name="desktop_notification_title">Kärgvõrgustik</string>
<string name="export_tak_data_package">Ekspordi TAK andmepakett</string>
<string name="clear_time_zone">Eemalda ajatsoon</string>
<string name="filter_icon">Filtreeri</string>
<string name="remove_filter">Eemalda filter</string>
<string name="show_iaq_legend">Näita õhukvaliteedi ajalugu</string>
<string name="action_show_message_status">Kuva sõnumi olek</string>
<string name="action_send_reply">Saada vastus</string>
<string name="action_copy_message">Kopeeri sõnum</string>
<string name="action_select_message">Vali sõnum</string>
<string name="action_delete_message">Kustuta sõnum</string>
<string name="action_react_with_emoji">Vasta emotikoniga</string>
<string name="action_select_device">Vali seade</string>
<string name="action_select_network">Vali võrk</string>
</resources>

View file

@ -383,9 +383,12 @@
<string name="userinfo">Käyttäjätiedot</string>
<string name="meshtastic_new_nodes_notifications">Uuden laitteen ilmoitukset</string>
<string name="snr">SNR</string>
<string name="snr_definition">Signaali-kohinasuhde (SNR) on mittari, jota käytetään viestinnässä halutun signaalin tason ja taustahälyn tason määrittämisessä. Meshtasticissa ja muissa langattomissa järjestelmissä korkeampi SNR tarkoittaa selkeämpää signaalia, joka voi parantaa tiedonsiirron luotettavuutta ja laatua.</string>
<string name="rssi">RSSI</string>
<string name="rssi_definition">Vastaanotetun signaalin voimakkuusindikaattori (RSSI) on mittari, jota käytetään määrittämään antennilla vastaanotetun signaalin voimakkuus. Korkeampi RSSI-arvo yleensä osoittaa vahvemman ja vakaamman yhteyden.</string>
<string name="iaq_definition">Sisäilman laatu (IAQ) on suhteellinen asteikko, jota voidaan mitata mm. Bosch BME680 anturilla ja sen arvoväli on 0500.</string>
<string name="device_metrics_log">Laitteen mittausloki</string>
<string name="node_map">Laitekartta</string>
<string name="position_log">Sijainti</string>
<string name="last_position_update">Viimeisin sijainnin päivitys</string>
<string name="env_metrics_log">Ympäristöarvot</string>
@ -432,6 +435,7 @@
<string name="one_month">1 kk</string>
<string name="max">Kaikki</string>
<string name="min">Minimi</string>
<string name="avg">Keskiarvo</string>
<string name="expand_chart">Laajenna kaavio</string>
<string name="collapse_chart">Pienennä kaavio</string>
<string name="unknown_age">Tuntematon ikä</string>
@ -609,23 +613,6 @@
<string name="ignore_mqtt">Ohita MQTT</string>
<string name="ok_to_mqtt">MQTT päällä</string>
<string name="mqtt_config">MQTT asetukset</string>
<string name="mqtt_status_inactive">Passiivinen</string>
<string name="mqtt_status_disconnected">Ei yhdistetty</string>
<string name="mqtt_status_disconnected_with_reason">Yhteys katkaistu — %1$s</string>
<string name="mqtt_status_connecting">Yhdistetään…</string>
<string name="mqtt_status_connected">Yhdistetty</string>
<string name="mqtt_status_reconnecting">Yhdistetään uudelleen…</string>
<string name="mqtt_status_reconnecting_with_attempt">Yhdistetään uudelleen (yritys %1$d) — %2$s</string>
<string name="mqtt_test_connection">Testaa yhteys</string>
<string name="mqtt_probe_running">Tarkistetaan välityspalvelinta…</string>
<string name="mqtt_probe_success">Yhteys onnistui. Välityspalvelin hyväksyi tunnistetiedot.</string>
<string name="mqtt_probe_success_with_info">Yhteys onnistui (%1$s)</string>
<string name="mqtt_probe_rejected">Välityspalvelin ei hyväksynyt: %1$s</string>
<string name="mqtt_probe_dns_failure">Palvelinta ei löytynyt</string>
<string name="mqtt_probe_tcp_failure">Yhteyttä välityspalvelimeen ei saada (TCP)</string>
<string name="mqtt_probe_tls_failure">TLS-yhteyden muodostus epäonnistui</string>
<string name="mqtt_probe_timeout">Aikakatkaistu %1$d ms jälkeen</string>
<string name="mqtt_probe_other_failure">Yhdistäminen epäonnistui</string>
<string name="mqtt_enabled">MQTT käytössä</string>
<string name="address">Osoite</string>
<string name="username">Käyttäjänimi</string>
@ -1222,21 +1209,4 @@
<string name="wifi_provision_ssid_placeholder">Syötä tai valitse verkko</string>
<string name="wifi_provision_status_applied">WiFi määritetty onnistuneesti!</string>
<string name="wifi_provision_status_failed">WiFi-asetusten käyttöönotto epäonnistui</string>
<string name="desktop_tray_tooltip">Meshtastic työpöytä</string>
<string name="desktop_tray_show">Näytä Meshtastic</string>
<string name="desktop_tray_quit">Lopeta</string>
<string name="desktop_notification_title">Meshtastic</string>
<string name="export_tak_data_package">Vie TAK-datapaketti</string>
<string name="clear_time_zone">Tyhjennä aikavyöhyke</string>
<string name="filter_icon">Suodatus</string>
<string name="remove_filter">Poista suodatin</string>
<string name="show_iaq_legend">Näytä ilmanlaadun selite</string>
<string name="action_show_message_status">Näytä viestin tila</string>
<string name="action_send_reply">Lähetä vastaus</string>
<string name="action_copy_message">Kopioi viesti</string>
<string name="action_select_message">Valitse viesti</string>
<string name="action_delete_message">Poista viesti</string>
<string name="action_react_with_emoji">Reaktio emojin kanssa</string>
<string name="action_select_device">Valitse laite</string>
<string name="action_select_network">Valitse verkko</string>
</resources>

View file

@ -18,7 +18,6 @@
<resources>
<string name="meshtastic_app_name">Meshtastic</string>
<!-- Language tags native names (not available via .getDisplayLanguage) -->
<string name="fallback_node_name">Meshtastic %1$s</string>
<string name="node_filter_placeholder">Filtre</string>
<string name="desc_node_filter_clear">Effacer le filtre de nœud</string>
<string name="node_filter_title">Filtrer par</string>
@ -41,11 +40,9 @@
<string name="internal">Interne</string>
<string name="node_sort_via_favorite">par Favoris</string>
<string name="node_filter_show_ignored">Afficher uniquement les nœuds ignorés</string>
<string name="node_filter_exclude_mqtt">Exclure MQTT</string>
<string name="unrecognized">Non reconnu</string>
<string name="message_status_enroute">En attente d'accusé de réception</string>
<string name="message_status_queued">En file d'attente pour l'envoi</string>
<string name="message_status_delivered">Délivré au nœud</string>
<string name="message_status_unknown">Inconnu</string>
<string name="message_status_sfpp_routing">Routage via chaîne SF++…</string>
<string name="message_status_sfpp_confirmed">Confirmé via chaîne SF++</string>
@ -122,8 +119,7 @@
<string name="config_position_broadcast_smart_minimum_distance_summary">Distance minimale en mètres pour considérer une diffusion de position intelligente.</string>
<string name="config_position_gps_update_interval_summary">À quelle fréquence devrions-nous essayer d'obtenir une position GPS (&lt;10sec le GPS est maintenu allumé).</string>
<string name="config_position_flags_summary">Champs optionnels à inclure dans les messages de position. Plus il y en a, plus le message est grand, plus cela augmentant le temps d'occupation du réseau et le risque de perte.</string>
<string name="config_power_is_power_saving_summary">Sera en veille profonde autant que possible, pour les rôles traceurs et capteur, cela inclura également la radio LoRa. N'utilisez pas ce paramètre si vous voulez utiliser votre appareil avec les applications de téléphone ou si vous utilisez un appareil sans bouton utilisateur.</string>
<string name="config_security_public_key">Généré à partir de votre clé publique et envoyé à d'autres nœuds sur le maillage pour leur permettre de calculer une clé secrète partagée.</string>
<string name="config_power_is_power_saving_summary">Sera en veille profonde autant que possible, pour les rôles traqueur et capteur, cela inclura également la radio LoRa. N'utilisez pas ce paramètre si vous voulez utiliser votre appareil avec les applications de téléphone ou si vous utilisez un appareil sans bouton utilisateur.</string>
<string name="config_security_private_key">Utilisée pour créer une clé partagée avec un appareil distant.</string>
<string name="config_security_admin_key">Clé publique autorisée à envoyer des messages dadministration à ce nœud.</string>
<string name="config_security_is_managed">L'appareil est géré par un administrateur de maillage, l'utilisateur ne peut accéder à aucun des paramètres de l'appareil.</string>
@ -167,25 +163,18 @@
<string name="ip_port">Port :</string>
<string name="connected">Connecté</string>
<string name="connection_status">Connexions actuelles :</string>
<string name="wifi_ip">IP du Wifi :</string>
<string name="wifi_ip">IP WiFi :</string>
<string name="ethernet_ip">IP Ethernet :</string>
<string name="connecting">Connexion en cours</string>
<string name="not_connected">Non connecté</string>
<string name="no_device_selected">Aucun appareil sélectionné</string>
<string name="unknown_device">Périphérique inconnu</string>
<string name="no_network_devices_found">Aucun périphérique réseau trouvé</string>
<string name="no_usb_devices_found">Pas de périphérique USB trouvé</string>
<string name="usb">USB</string>
<string name="demo_mode">Mode Démo</string>
<string name="connected_sleeping">Connecté à la radio, mais en mode veille</string>
<string name="app_too_old">Mise à jour de lapplication requise</string>
<string name="must_update">Vous devez mettre à jour cette application sur l'app store (ou Github). Il est trop vieux pour dialoguer avec le micrologiciel de la radio. Veuillez lire nos <a href="https://meshtastic.org/docs/software/android/installation"> docs</a> sur ce sujet.</string>
<string name="none">Aucun (désactivé)</string>
<string name="meshtastic_service_notifications">Notifications de service</string>
<string name="acknowledgements">Remerciements</string>
<string name="open_source_libraries">Bibliothèques Open Source</string>
<string name="open_source_description">Meshtastic est construit avec les bibliothèques open source suivantes. Appuyez sur n'importe quelle bibliothèque pour voir sa licence.</string>
<string name="library_count">%1$d Bibliothèques</string>
<string name="channel_invalid">Cette URL de canal est invalide et ne peut pas être utilisée</string>
<string name="debug_panel">Panneau de débogage</string>
<string name="debug_decoded_payload">Contenu décodé :</string>
@ -218,21 +207,7 @@
<string name="match_all">Correspondre à tout | N'importe quel</string>
<string name="debug_clear_logs_confirm">Cela supprimera tous les paquets de journaux et les entrées de la base de données de votre appareil - c'est une réinitialisation complète, et est permanent.</string>
<string name="clear">Effacer</string>
<string name="search_emoji">Rechercher des émojis...</string>
<string name="more_reactions">Plus d'actions</string>
<string name="channel_label">Canal</string>
<string name="a11y_label_value">%1$s: %2$s</string>
<string name="a11y_message_from">Message de %1$s: %2$s</string>
<string name="preview_header">Entête</string>
<string name="preview_item">Élément %1$d</string>
<string name="preview_footer">Pied de page</string>
<string name="preview_pill">Exporter le paquet de données TAK</string>
<string name="preview_dot">Point</string>
<string name="preview_text">Texte</string>
<string name="preview_gauge">Jauge</string>
<string name="preview_gradient">Dégradé</string>
<string name="preview_custom_composable_line_one">Ceci est un composable personnalisé</string>
<string name="preview_custom_composable_line_two">Avec plusieurs lignes et styles</string>
<string name="message_delivery_status">Statut d'envoi du message</string>
<string name="new_messages_below">Nouveaux messages au-dessous</string>
<string name="meshtastic_messages_notifications">Notifications de message</string>
@ -253,15 +228,10 @@
<string name="reset_to_defaults">Rétablir les valeurs par défaut</string>
<string name="apply">Appliquer</string>
<string name="theme">Thème</string>
<string name="contrast">Contraste</string>
<string name="theme_light">Clair</string>
<string name="theme_dark">Sombre</string>
<string name="theme_system">Valeur par défaut du système</string>
<string name="choose_theme">Choisir un thème</string>
<string name="choose_contrast">Niveau de contraste</string>
<string name="contrast_standard">Standard</string>
<string name="contrast_medium">Milieu</string>
<string name="contrast_high">Haut</string>
<string name="provide_location_to_mesh">Fournir l'emplacement au maillage</string>
<string name="use_homoglyph_characters_encoding">Encodage compact pour Cyrillique</string>
<plurals name="delete_messages">
@ -305,7 +275,6 @@
<string name="direct_message">Message direct</string>
<string name="nodedb_reset">Reconfiguration de NodeDB</string>
<string name="delivery_confirmed">Réception confirmée par le destinataire</string>
<string name="delivery_confirmed_reboot_warning">Votre appareil peut se déconnecter et redémarrer lorsque les paramètres sont appliqués.</string>
<string name="error">Erreur</string>
<string name="unknown_error">Une erreur inconnue s'est produite</string>
<string name="ignore">Ignorer</string>
@ -348,8 +317,6 @@
<string name="currently">Actuellement :</string>
<string name="mute_status_always">Toujours muet</string>
<string name="mute_status_unmuted">Non muet</string>
<string name="mute_status_muted_for_days">Muet pour %1$d jours, %2$s heures</string>
<string name="mute_status_muted_for_hours">Muet pour %1$s heures</string>
<string name="mute_add">Désactiver les notifications pour '%1$s' ?</string>
<string name="mute_remove">Réactiver les notifications pour '%1$s' ?</string>
<string name="replace">Remplacer</string>
@ -359,10 +326,7 @@
<string name="battery">Batterie</string>
<string name="channel_utilization">UtilCanal</string>
<string name="air_utilization">UtilAir</string>
<string name="device_metrics_percent_value">%1$s / %2$s%%</string>
<string name="device_metrics_voltage_value">%1$s: %2$s V</string>
<string name="device_metrics_numeric_value">%1$s</string>
<string name="device_metrics_label_value">%1$s: %2$s</string>
<string name="temperature">Temp</string>
<string name="humidity">Hum</string>
<string name="soil_temperature">Temp sol</string>
@ -383,9 +347,12 @@
<string name="userinfo">Infos utilisateur</string>
<string name="meshtastic_new_nodes_notifications">Notifikasyon nouvo nœud</string>
<string name="snr">SNR</string>
<string name="snr_definition">Signal-to-Noise Ratio, une mesure utilisée dans les communications pour quantifier le niveau du signal par rapport au niveau du bruit de fond. Dans les systèmes Meshtastic et autres systèmes sans fil, un SNR plus élevé indique un signal plus clair qui peut améliorer la fiabilité et la qualité de la transmission de données.</string>
<string name="rssi">RSSI</string>
<string name="rssi_definition">Indicateur de force du signal reçu, une mesure utilisée pour déterminer le niveau de puissance reçu par l'antenne. Une valeur RSSI plus élevée indique généralement une connexion plus forte et plus stable.</string>
<string name="iaq_definition">(Qualité de l'air intérieur) valeur de l'échelle relative IAQ mesurée par Bosch BME680. Plage de valeur 0500.</string>
<string name="device_metrics_log">Métriques de lappareil</string>
<string name="node_map">Carte historique des positions</string>
<string name="position_log">Position</string>
<string name="last_position_update">Dernière mise à jour de position</string>
<string name="env_metrics_log">Métriques d'environnement</string>
@ -414,26 +381,13 @@
<string name="traceroute_duration">Durée : %1$s s</string>
<string name="traceroute_route_towards_dest">Route aller :\n\n</string>
<string name="traceroute_route_back_to_us">Route retour :\n\n</string>
<string name="traceroute_forward_hops">Saut vers l'avant</string>
<string name="traceroute_return_hops">Saut vers l'arrière</string>
<string name="traceroute_round_trip">Aller/Retour</string>
<string name="traceroute_no_response">Pas de réponse</string>
<string name="load_1_min">Charge 1m</string>
<string name="load_5_min">Charge 5m</string>
<string name="load_15_min">Charge 15m</string>
<string name="load_1_min_description">Moyenne de charge du système d'une minute</string>
<string name="load_5_min_description">Moyenne de charge du système de cinq minutes</string>
<string name="load_15_min_description">Moyenne de charge du système de 15 minutes</string>
<string name="free_memory_description">Mémoire système disponible en octets</string>
<string name="one_hour_short">1H</string>
<string name="twenty_four_hours">24H</string>
<string name="one_week">1S</string>
<string name="two_weeks">2S</string>
<string name="one_month">1M</string>
<string name="max">Max</string>
<string name="min">Min</string>
<string name="expand_chart">Agrandir le graphique</string>
<string name="collapse_chart">Réduire le graphique</string>
<string name="unknown_age">Age inconnu</string>
<string name="copy">Copier</string>
<string name="alert_bell_text">Caractère d'appel !</string>
@ -447,17 +401,11 @@
<string name="channel_1">Canal 1</string>
<string name="channel_2">Canal 2</string>
<string name="channel_3">Canal 3</string>
<string name="channel_4">Canal 4</string>
<string name="channel_5">Canal 5</string>
<string name="channel_6">Canal 6</string>
<string name="channel_7">Canal 7</string>
<string name="channel_8">Canal 8</string>
<string name="current">Actif</string>
<string name="voltage">Tension</string>
<string name="are_you_sure">Êtes-vous sûr ?</string>
<string name="router_role_confirmation_text"><![CDATA[J'ai lu la <a href="https://meshtastic.org/docs/configuration/radio/device/#roles">Documentation du rôle de l'appareil</a> et le billet de blog sur comment <a href="http://meshtastic.org/blog/choosing-the-right-device-role">Choisir le rôle de l'appareil approprié</a>.]]></string>
<string name="i_know_what_i_m_doing">Je sais ce que je fais.</string>
<string name="low_battery_message">La batterie du nœud %1$s est faible (%2$d%)</string>
<string name="meshtastic_low_battery_notifications">Notifications de batterie faible</string>
<string name="low_battery_title">Batterie faible : %1$s</string>
<string name="meshtastic_low_battery_temporary_remote_notifications">Notifications de batterie faible (nœuds favoris)</string>
@ -583,9 +531,6 @@
<string name="output_duration_milliseconds">Durée de sortie (en millisecondes)</string>
<string name="nag_timeout_seconds">Durée de répétition de la sortie (secondes)</string>
<string name="ringtone">Sonnerie</string>
<string name="ringtone_imported">Sonnerie importée</string>
<string name="ringtone_file_empty">Le fichier est vide</string>
<string name="ringtone_import_error">Erreur d'importation : %1$s</string>
<string name="play">Lancer</string>
<string name="use_i2s_as_buzzer">Utiliser l'I2S comme buzzer</string>
<string name="lora_config">LoRa</string>
@ -609,13 +554,6 @@
<string name="ignore_mqtt">Ignorer MQTT</string>
<string name="ok_to_mqtt">Transmission des paquets vers MQTT</string>
<string name="mqtt_config">Configuration MQTT</string>
<string name="mqtt_status_inactive">Inactif</string>
<string name="mqtt_status_disconnected">Déconnecté</string>
<string name="mqtt_status_connecting">Connexion…</string>
<string name="mqtt_status_connected">Connecté</string>
<string name="mqtt_status_reconnecting">Reconnexion…</string>
<string name="mqtt_test_connection">Test de la connexion</string>
<string name="mqtt_probe_other_failure">Échec de la connexion</string>
<string name="mqtt_enabled">MQTT activé</string>
<string name="address">Adresse</string>
<string name="username">Nom d'utilisateur</string>
@ -687,8 +625,6 @@
<string name="serial_enabled">Série activée</string>
<string name="echo_enabled">Écho activé</string>
<string name="serial_baud_rate">Vitesse de transmission série</string>
<string name="serial_rx_pin">RX</string>
<string name="serial_tx_pin">Tx</string>
<string name="timeout">Délai d'expiration</string>
<string name="serial_mode">Mode série</string>
<string name="override_console_serial_port">Outrepasser le port série de la console</string>
@ -723,15 +659,8 @@
<string name="distance">Distance</string>
<string name="lux">Lux</string>
<string name="wind">Vent</string>
<string name="wind_speed">Vitesse du vent</string>
<string name="wind_gust">Rafales de vent</string>
<string name="wind_lull">Vent à la traîne</string>
<string name="wind_direction">Direction du vent</string>
<string name="rainfall_1h">Pluie (1h)</string>
<string name="rainfall_24h">Pluie (24h)</string>
<string name="weight">Poids</string>
<string name="radiation">Radiation</string>
<string name="one_wire_temperature">Températeur 1-Wire</string>
<string name="store_forward_config"><![CDATA[Paramètres Stocker & Transférer]]></string>
<string name="indoor_air_quality_iaq">Qualité de l'air intérieur (IAQ)</string>
<string name="url">URL</string>
@ -748,7 +677,6 @@
<string name="timestamp">Horodatage</string>
<string name="heading">En-tête</string>
<string name="speed">Vitesse</string>
<string name="speed_kmh">%1$d Km/h</string>
<string name="sats">Sats</string>
<string name="alt">Alt</string>
<string name="freq">Fréq</string>
@ -814,11 +742,6 @@
<string name="show_waypoints">Afficher les points de repère</string>
<string name="show_precision_circle">Afficher les cercles de précision</string>
<string name="client_notification">Notification client</string>
<string name="key_verification_title">Vérification de la clé</string>
<string name="key_verification_request_title">Requête de vérification de clé</string>
<string name="key_verification_final_title">Vérification de la clé terminée</string>
<string name="duplicated_public_key_title">Clé publique dupliquée détectée</string>
<string name="low_entropy_key_title">Clé de chiffrement faible détectée</string>
<string name="compromised_keys">Clés compromises détectées, sélectionnez OK pour régénérer.</string>
<string name="regenerate_private_key">Régénérer la clé privée</string>
<string name="regenerate_keys_confirmation">Êtes-vous sûr de vouloir régénérer votre clé privée ?\n\nLes nœuds qui peuvent avoir précédemment échangé des clés avec ce nœud devront supprimer ce nœud et ré-échanger des clés afin de reprendre une communication sécurisée.</string>
@ -871,14 +794,7 @@
<string name="type_a_message">Composer un message</string>
<string name="pax_metrics_log">Métriques de PAX</string>
<string name="pax">PAX</string>
<string name="pax_total_format">PAX : %1$d</string>
<string name="pax_ble_format">B:%1$d</string>
<string name="pax_wifi_format">W :%1$d</string>
<string name="pax_total_marker">PAX : %1$s</string>
<string name="pax_ble_marker">BLE: %1$s</string>
<string name="pax_wifi_marker">Wi-Fi : %1$s</string>
<string name="no_pax_metrics_logs">Aucune métrique PAX disponible.</string>
<string name="wifi_devices">Approvisionnement Wi-Fi pour mPWRD-OS</string>
<string name="ble_devices">Appareils Bluetooth</string>
<string name="connected_device">Périphérique connecté</string>
<string name="routing_error_rate_limit_exceeded">Limite de débit dépassée. Veuillez réessayer plus tard.</string>
@ -933,8 +849,6 @@
<string name="map_type_terrain">Terrain</string>
<string name="map_type_hybrid">Hybride</string>
<string name="manage_map_layers">Gérer les calques de la carte</string>
<string name="map_layer_formats">Les calques personnalisés prennent en charge les fichiers .kml, .kmz ou GeoJSON.</string>
<string name="no_map_layers_loaded">Aucun calque personnalisé chargé.</string>
<string name="hide_layer">Ajouter un calque</string>
<string name="show_layer">Afficher le calque</string>
<string name="remove_layer">Supprimer le calque</string>
@ -942,10 +856,6 @@
<string name="nodes_at_this_location">Nœuds à cet emplacement</string>
<string name="selected_map_type">Type de carte sélectionné</string>
<string name="manage_custom_tile_sources">Gérer les sources de tuiles personnalisées</string>
<string name="add_custom_tile_source">Ajouter un réseau de tuile personnalisée</string>
<string name="no_custom_tile_sources_found">Aucune source de tuiles personnalisées trouvée.</string>
<string name="edit_custom_tile_source">Modifier le réseau de tuile personnalisée</string>
<string name="delete_custom_tile_source">Supprimer le réseau de tuile personnalisée</string>
<string name="name_cannot_be_empty">Le nom ne peut pas être vide.</string>
<string name="provider_name_exists">Le nom du fournisseur existe déjà.</string>
<string name="url_cannot_be_empty">URL ne peut être vide.</string>
@ -1039,7 +949,6 @@
<string name="firmware_update_release_notes">Notes de Version</string>
<string name="firmware_update_unknown_error">Une erreur inconnue s'est produite</string>
<string name="firmware_update_node_info_missing">Les informations de l'utilisateur du nœud sont manquantes.</string>
<string name="firmware_update_battery_low">Batterie trop faible (%1$d%). Veuillez charger votre appareil avant de mettre à jour.</string>
<string name="firmware_update_retrieval_failed">Impossible de récupérer le fichier firmware.</string>
<string name="firmware_update_usb_failed">Échec de la mise à jour USB</string>
<string name="firmware_update_hash_rejected">Intégrité (hash) du firmware rejetée. Veuillez réessayer ou mettre à jours l'appareil via USB.</string>
@ -1116,10 +1025,8 @@
<string name="bluetooth_feature_config">Configuration</string>
<string name="bluetooth_feature_config_description">Gérer à distance sans fil les paramètres et les canaux de votre appareil.</string>
<string name="map_style_selection">Sélection du style de carte</string>
<string name="local_stats_battery">Batterie : %1$d%</string>
<string name="local_stats_nodes">Nœuds : %1$d en ligne / %2$d au total</string>
<string name="local_stats_uptime">Temps de disponibilité : %1$s</string>
<string name="local_stats_utilization">ChUtil: %1$s% | AirTX: %2$s%</string>
<string name="local_stats_traffic">Trafic : TX %1$d / RX %2$d (D: %3$d)</string>
<string name="local_stats_relays">Relais : %1$d (annulé: %2$d)</string>
<string name="local_stats_diagnostics_prefix">Diagnostiques : %1$s</string>
@ -1133,94 +1040,10 @@
<string name="refresh">Actualiser</string>
<string name="updated">Mis à jour</string>
<!-- Network Map Layers -->
<string name="add_network_layer">Ajouter une couche de réseau</string>
<string name="local_mbtiles_file">Fichier local MBTiles</string>
<string name="add_local_mbtiles_file">Ajouter un fichier local MBTiles</string>
<string name="tak">TAK (ATAK)</string>
<string name="tak_config">Configuration TAK</string>
<string name="tak_server_enabled">Activer le serveur TAK local</string>
<string name="tak_server_enabled_desc">Démarre un serveur TCP sur le port 8089 pour les connexions ATAK</string>
<string name="tak_team">Couleur de l'équipe</string>
<string name="tak_role">Rôle Membre</string>
<string name="tak_team_unspecified_color">Non spécifié</string>
<string name="tak_team_white">Blanc</string>
<string name="tak_team_yellow">Jaune</string>
<string name="tak_team_orange">Orange</string>
<string name="tak_team_magenta">Magenta</string>
<string name="tak_team_red">Rouge</string>
<string name="tak_team_maroon">Marron</string>
<string name="tak_team_purple">Pourpre</string>
<string name="tak_team_dark_blue">Bleu foncé</string>
<string name="tak_team_blue">Bleu</string>
<string name="tak_team_cyan">Cyan</string>
<string name="tak_team_teal">Turquoise</string>
<string name="tak_team_green">Vert</string>
<string name="tak_team_dark_green">Vert Foncé</string>
<string name="tak_team_brown">Marron</string>
<string name="tak_role_unspecified">Non spécifié</string>
<string name="tak_role_teammember">Membre de l'équipe</string>
<string name="tak_role_teamlead">Chef d'équipe</string>
<string name="tak_role_hq">Quartier général</string>
<string name="tak_role_sniper">Tireur d'élite</string>
<string name="tak_role_medic">Medic</string>
<string name="tak_role_forwardobserver">Observateur de transfert</string>
<string name="tak_role_rto">Opérateur de radio téléphonie</string>
<string name="tak_role_k9">Doggo (K9)</string>
<string name="traffic_management">Gestion du trafic</string>
<string name="traffic_management_config">Configuration de la gestion du trafic</string>
<string name="traffic_management_enabled">Module activé</string>
<string name="traffic_management_position_dedup">Déduplication de Position</string>
<string name="traffic_management_position_precision">Précision de position (octets)</string>
<string name="traffic_management_position_min_interval">Intervalle de position min (secs)</string>
<string name="traffic_management_nodeinfo_direct_response">Réponse directe de NodeInfo</string>
<string name="traffic_management_nodeinfo_direct_response_max_hops">Max de saut pour une réponse directe</string>
<string name="traffic_management_rate_limit_enabled">Limitation de débit</string>
<string name="traffic_management_rate_limit_window">Fenêtre de limitation de taux (secs)</string>
<string name="traffic_management_rate_limit_max_packets">Paquets maximum dans la fenêtre</string>
<string name="traffic_management_drop_unknown_enabled">Ignorer les paquets inconnus</string>
<string name="traffic_management_unknown_packet_threshold">Seuil de paquets inconnu</string>
<string name="traffic_management_exhaust_hop_telemetry">Télémétrie locale uniquement (Relays)</string>
<string name="traffic_management_exhaust_hop_position">Position locale uniquement (Relays)</string>
<string name="traffic_management_router_preserve_hops">Conserver les sauts du Routeur</string>
<string name="note">Note</string>
<string name="device_storage_ui_title">Stockage de l'appareil &amp; UI (lecture seule)</string>
<string name="device_theme_language">Thème %1$s, Langue %2$s</string>
<string name="files_available">Fichiers disponibles (%1$d ) :</string>
<string name="file_entry">- %1$s (%2$d octets)</string>
<string name="no_files_manifested">Aucun fichier affiché.</string>
<string name="connect">Connecter</string>
<string name="done">Terminé</string>
<string name="wifi_provisioning">Approvisionnement Wi-Fi pour mPWRD-OS</string>
<string name="wifi_provision_description">Fournissez les identifiants Wi-Fi à votre appareil mPWRD-OS via Bluetooth.</string>
<string name="wifi_provision_mpwrd_disclaimer">En savoir plus sur le projet mPWRD-OS\nhttps://github.com/mPWRD-OS</string>
<string name="wifi_provision_scanning_ble">Recherche de l'appareil</string>
<string name="wifi_provision_device_found">Appareil détecté</string>
<string name="wifi_provision_device_found_detail">Prêt à rechercher des réseaux WiFi.</string>
<string name="wifi_provision_scan_networks">Rechercher des réseaux</string>
<string name="wifi_provision_scanning_wifi">Recherche…</string>
<string name="wifi_provision_sending_credentials">Application de la configuration WiFi…</string>
<string name="wifi_provision_no_networks">Aucun réseau trouvé</string>
<string name="wifi_provision_connect_failed">Impossible de se connecter : %1$s</string>
<string name="wifi_provision_scan_failed">Échec de la recherche des réseaux WiFi : %1$s</string>
<string name="wifi_provision_signal_strength">%1$d%</string>
<string name="wifi_provision_available_networks">Réseaux disponibles</string>
<string name="wifi_provision_ssid_label">Nom du réseau (SSID)</string>
<string name="wifi_provision_ssid_placeholder">Saisir ou sélectionnez un réseau</string>
<string name="wifi_provision_status_applied">WiFi configuré avec succès !</string>
<string name="wifi_provision_status_failed">Impossible d'appliquer la configuration WiFi</string>
<string name="desktop_tray_tooltip">Meshtastic application de bureau</string>
<string name="desktop_tray_show">Afficher Meshtastic</string>
<string name="desktop_tray_quit">Quitter</string>
<string name="desktop_notification_title">Meshtastic</string>
<string name="export_tak_data_package">Exporter le paquet de données TAK</string>
<string name="filter_icon">Filtre</string>
<string name="remove_filter">Supprimer le filtre</string>
<string name="action_show_message_status">Afficher le statut du message</string>
<string name="action_send_reply">Envoyer une réponse</string>
<string name="action_copy_message">Copier le message</string>
<string name="action_select_message">Sélectionner le message</string>
<string name="action_delete_message">Supprimer le message</string>
<string name="action_react_with_emoji">Réagir avec un emoji</string>
<string name="action_select_device">Sélectionner l'appareil</string>
<string name="action_select_network">Sélectionner le réseau</string>
</resources>

View file

@ -190,7 +190,10 @@
<string name="encryption_pkc">Cóid Poiblí Eochair</string>
<string name="encryption_error">Mícomhoiriúnacht na heochrach phoiblí</string>
<string name="meshtastic_new_nodes_notifications">Fógartha faoi na nodes nua</string>
<string name="snr_definition">Ráta Sigineal go Torann, tomhas a úsáidtear i gcomhfhreagras chun an leibhéal de shígnéil inmhianaithe agus torann cúlra a mheas. I Meshtastic agus i gcórais gan sreang eile, ciallaíonn SNR níos airde go bhfuil sígneál níos soiléire ann agus ábalta méadú ar chreideamh agus cáilíocht an tarchur sonraí.</string>
<string name="rssi_definition">Táscaire Cumhachta Athnuachana Aithint an Aoise, tomhas a úsáidtear chun leibhéal cumhachta atá faighte ag an antsnáithe a mheas. Léiríonn RSSI níos airde gnóthachtáil níos laige atá i gceangal seasmhach agus níos láidre.</string>
<string name="iaq_definition">(Cáilíocht Aeir Inmheánach) scála ábhartha den luach QAÍ a thomhas ag Bosch BME680. Scála Luach 0500.</string>
<string name="node_map">Léarscáil an Node</string>
<string name="administration">Rialachas</string>
<string name="remote_admin">Rialú iargúlta</string>
<string name="bad">Go dona</string>
@ -210,7 +213,6 @@
</plurals>
<string name="traceroute_diff">Céimeanna i dtreo %1$d Céimeanna ar ais %2$d</string>
<string name="region_frequency_plan">Réigiún</string>
<string name="mqtt_status_disconnected">Na ceangailte</string>
<string name="timeout">Am tráth</string>
<string name="distance">Sáth</string>
<!-- INSECURE_NO_PRECISE State (Yellow Open Lock) -->
@ -227,5 +229,4 @@
<!-- Compass -->
<!-- Message Filter -->
<!-- Network Map Layers -->
<string name="filter_icon">Scagaire</string>
</resources>

View file

@ -149,7 +149,6 @@
<string name="mute_always">Sempre</string>
<string name="traceroute_log">Traza-ruta</string>
<string name="region_frequency_plan">Rexión</string>
<string name="mqtt_status_disconnected">Desconectado</string>
<string name="distance">Distancia</string>
<!-- INSECURE_NO_PRECISE State (Yellow Open Lock) -->
<!-- INSECURE_PRECISE_ONLY State (Red Open Lock) -->
@ -165,5 +164,4 @@
<!-- Compass -->
<!-- Message Filter -->
<!-- Network Map Layers -->
<string name="filter_icon">Filtro</string>
</resources>

View file

@ -133,7 +133,6 @@
<string name="traceroute_log">בדיקת מסלול</string>
<string name="messages">הודעות</string>
<string name="region_frequency_plan">אזור</string>
<string name="mqtt_status_disconnected">מנותק</string>
<string name="distance">מרחק</string>
<string name="bottom_nav_settings">הגדרות</string>
<!-- INSECURE_NO_PRECISE State (Yellow Open Lock) -->
@ -148,5 +147,4 @@
<!-- Compass -->
<!-- Message Filter -->
<!-- Network Map Layers -->
<string name="filter_icon">פילטר</string>
</resources>

View file

@ -150,7 +150,6 @@
<string name="details">Detalji</string>
<string name="red">Crveno</string>
<string name="region_frequency_plan">Regija</string>
<string name="mqtt_status_disconnected">Odspojeno</string>
<string name="distance">Udaljenost</string>
<string name="meshtastic">Meshtastic</string>
<!-- INSECURE_NO_PRECISE State (Yellow Open Lock) -->
@ -168,6 +167,4 @@
<!-- Message Filter -->
<!-- Network Map Layers -->
<string name="tak_team_red">Crveno</string>
<string name="desktop_notification_title">Meshtastic</string>
<string name="filter_icon">Filtriraj</string>
</resources>

View file

@ -186,7 +186,10 @@
<string name="encryption_pkc">Chifreman Kle Piblik</string>
<string name="encryption_error">Pa matche kle piblik</string>
<string name="meshtastic_new_nodes_notifications">Notifikasyon nouvo nœud</string>
<string name="snr_definition">Rapò Siynal sou Bri, yon mezi ki itilize nan kominikasyon pou mezire nivo siynal vle a kont nivo bri ki nan anviwònman an. Nan Meshtastic ak lòt sistèm san fil, yon SNR pi wo endike yon siynal pi klè ki ka amelyore fyab ak kalite transmisyon done.</string>
<string name="rssi_definition">Endikatè Fòs Siynal Resevwa, yon mezi ki itilize pou detèmine nivo pouvwa siynal ki resevwa pa antèn nan. Yon RSSI pi wo jeneralman endike yon koneksyon pi fò ak plis estab.</string>
<string name="iaq_definition">(Kalite Lèy Entèryè) echèl relatif valè IAQ jan li mezire pa Bosch BME680. Ranje valè 0500.</string>
<string name="node_map">Kat Nœud</string>
<string name="administration">Administrasyon</string>
<string name="remote_admin">Administrasyon Remote</string>
<string name="bad">Move</string>
@ -198,7 +201,6 @@
<string name="traceroute_direct">Direk</string>
<string name="traceroute_diff">Hops vèsus %1$d Hops tounen %2$d</string>
<string name="region_frequency_plan">Rejyon</string>
<string name="mqtt_status_disconnected">Dekonekte</string>
<string name="timeout">Tan pase</string>
<string name="distance">Distans</string>
<!-- INSECURE_NO_PRECISE State (Yellow Open Lock) -->
@ -215,5 +217,4 @@
<!-- Compass -->
<!-- Message Filter -->
<!-- Network Map Layers -->
<string name="filter_icon">Filtre</string>
</resources>

View file

@ -316,9 +316,12 @@
<string name="encryption_error">Publikus kulcs nem egyezik</string>
<string name="meshtastic_new_nodes_notifications">Új állomás értesítések</string>
<string name="snr">SNR</string>
<string name="snr_definition">Jelzaj arány (SNR): a kommunikációban a kívánt jel szintjének és a háttérzaj szintjének aránya. A Meshtastic és más vezeték nélküli rendszerek esetében a magasabb SNR tisztább jelet jelent, ami javítja az adatátvitel megbízhatóságát és minőségét.</string>
<string name="rssi">RSSI</string>
<string name="rssi_definition">Vett jelerősség-mutató (RSSI): az antenna által vett jel teljesítményszintjének mérése. A magasabb RSSI általában erősebb, stabilabb kapcsolatot jelez.</string>
<string name="iaq_definition">(Beltéri levegőminőség) relatív IAQ érték a Bosch BME680 szenzor alapján. Értéktartomány: 0500.</string>
<string name="device_metrics_log">Eszközmetrikák</string>
<string name="node_map">Állomás Térkép</string>
<string name="position_log">Pozíció</string>
<string name="last_position_update">Utolsó pozíciófrissítés</string>
<string name="env_metrics_log">Környezeti metrikák</string>
@ -512,8 +515,6 @@
<string name="ignore_mqtt">MQTT figyelmen kívül hagyása</string>
<string name="ok_to_mqtt">MQTT-re továbbítható</string>
<string name="mqtt_config">MQTT beállítások</string>
<string name="mqtt_status_disconnected">Szétkapcsolva</string>
<string name="mqtt_status_connected">Csatlakoztatva</string>
<string name="mqtt_enabled">MQTT engedélyezve</string>
<string name="address">Cím</string>
<string name="username">Felhasználónév</string>
@ -849,6 +850,4 @@
<string name="tak_team_blue">Kék</string>
<string name="tak_team_green">Zöld</string>
<string name="connect">Csatlakozás</string>
<string name="desktop_notification_title">Meshtastic</string>
<string name="filter_icon">Filter</string>
</resources>

View file

@ -119,7 +119,6 @@
<string name="error_duty_cycle">Hámarsksendingartíma náð. Ekki hægt að senda skilaboð, vinsamlegast reynið aftur síðar.</string>
<string name="traceroute_log">Ferilkönnun</string>
<string name="region_frequency_plan">Svæði</string>
<string name="mqtt_status_disconnected">Aftengd</string>
<!-- INSECURE_NO_PRECISE State (Yellow Open Lock) -->
<!-- INSECURE_PRECISE_ONLY State (Red Open Lock) -->
<!-- INSECURE_PRECISE_MQTT_WARNING State (Red Open Lock with Warning Badge) -->

View file

@ -355,9 +355,12 @@
<string name="userinfo">Informazioni Utente</string>
<string name="meshtastic_new_nodes_notifications">Notifiche di nuovi nodi</string>
<string name="snr">SNR</string>
<string name="snr_definition">Rapporto segnale-rumore (Signal-to-Noise Ratio), una misura utilizzata nelle comunicazioni per quantificare il livello di un segnale desiderato rispetto al livello di rumore di fondo. In Meshtastic e in altri sistemi wireless, un SNR più elevato indica un segnale più chiaro che può migliorare l'affidabilità e la qualità della trasmissione dei dati.</string>
<string name="rssi">RSSI</string>
<string name="rssi_definition">Indicatore di forza del segnale ricevuto (Received Signal Strength Indicator), una misura utilizzata per determinare il livello di potenza ricevuto dall'antenna. Un valore RSSI più elevato indica generalmente una connessione più forte e più stabile.</string>
<string name="iaq_definition">(Qualità dell'aria interna) scala relativa del valore della qualità dell'aria indoor, misurato da Bosch BME680. Valore Intervallo 0500.</string>
<string name="device_metrics_log">Metriche Dispositivo</string>
<string name="node_map">Mappa Dei Nodi</string>
<string name="position_log">Posizione</string>
<string name="last_position_update">Aggiornamento ultima posizione</string>
<string name="env_metrics_log">Metriche Ambientali</string>
@ -557,8 +560,6 @@
<string name="ignore_mqtt">Ignora MQTT</string>
<string name="ok_to_mqtt">OK per MQTT</string>
<string name="mqtt_config">Configurazione MQTT</string>
<string name="mqtt_status_disconnected">Disconnesso</string>
<string name="mqtt_status_connected">Connesso</string>
<string name="mqtt_enabled">MQTT abilitato</string>
<string name="address">Indirizzo</string>
<string name="username">Username</string>
@ -959,6 +960,4 @@
<string name="note">Note</string>
<string name="connect">Connetti</string>
<string name="done">Fatto</string>
<string name="desktop_notification_title">Meshtastic</string>
<string name="filter_icon">Filtro</string>
</resources>

View file

@ -275,8 +275,11 @@
<string name="encryption_error">公開キーが一致しません</string>
<string name="meshtastic_new_nodes_notifications">新しいノードの通知</string>
<string name="snr">SN比</string>
<string name="snr_definition">信号対イズ比SN比は、通信において、目的の信号のレベルを背景イズのレベルに対して定量化するために使用される尺度です。Meshtasticや他の無線システムでは、SN比が高いほど信号が鮮明であることを示し、データ伝送の信頼性と品質を向上させることができます。</string>
<string name="rssi">RSSI</string>
<string name="rssi_definition">受信信号強度インジケーターRSSIは、アンテナで受信している電力レベルを測定するための指標です。一般的にRSSI値が高いほど、より強力で安定した接続を示します。</string>
<string name="iaq_definition">(屋内空気品質) 相対スケールIAQ値は、ボッシュBME680によって測定されます。 値の範囲は 0-500。</string>
<string name="node_map">ノードマップ</string>
<string name="position_log">位置</string>
<string name="administration">管理</string>
<string name="remote_admin">リモート管理</string>
@ -421,8 +424,6 @@
<string name="pa_fan_disabled">PAファン無効</string>
<string name="ignore_mqtt">MQTT を無視</string>
<string name="mqtt_config">MQTT設定</string>
<string name="mqtt_status_disconnected">切断</string>
<string name="mqtt_status_connected">接続済</string>
<string name="mqtt_enabled">MQTTを有効化</string>
<string name="address">アドレス</string>
<string name="username">ユーザー名</string>
@ -651,6 +652,4 @@
<string name="traffic_management_config">トラフィック管理設定</string>
<string name="traffic_management_enabled">モジュール有効</string>
<string name="connect">接続</string>
<string name="desktop_notification_title">Meshtastic</string>
<string name="filter_icon">絞り込み</string>
</resources>

View file

@ -213,8 +213,11 @@
<string name="encryption_error">공개 키가 일치하지 않습니다</string>
<string name="meshtastic_new_nodes_notifications">새로운 노드 알림</string>
<string name="snr">SNR</string>
<string name="snr_definition">통신에서 원하는 신호의 수준을 배경 잡음의 수준과 비교하여 정량화하는 데 사용되는 신호 대 잡음비 Signal-to-Noise Ratio, SNR는 Meshtastic와 같은 무선 시스템에서 SNR이 높을수록 더 선명한 신호를 나타내어 데이터 전송의 안정성과 품질을 향상시킬 수 있습니다.</string>
<string name="rssi">RSSI</string>
<string name="rssi_definition">수신 신호 강도 지표 Received Signal Strength Indicator, RSSI는 안테나가 수신하는 신호의 전력 수준을 측정하는 데 사용되는 지표입니다. RSSI 값이 높을수록 일반적으로 더 강력하고 안정적인 연결을 나타냅니다.</string>
<string name="iaq_definition">(실내공기질) Bosch BME680으로 측정한 상대적 척도 IAQ 값. 범위 0500.</string>
<string name="node_map">노드 지도</string>
<string name="position_log">위치</string>
<string name="last_position_update">최근 위치 업데이트</string>
<string name="administration">관리</string>
@ -370,8 +373,6 @@
<string name="pa_fan_disabled">PA fan 비활성화됨</string>
<string name="ignore_mqtt">MQTT로 부터 수신 무시</string>
<string name="mqtt_config">MQTT 설정</string>
<string name="mqtt_status_disconnected">연결 끊김</string>
<string name="mqtt_status_connected">연결됨</string>
<string name="mqtt_enabled">MQTT 활성화</string>
<string name="address">서버 주소</string>
<string name="username">사용자명</string>
@ -538,6 +539,4 @@
<string name="tak_team_blue">파랑</string>
<string name="tak_team_green">초록</string>
<string name="connect">연결</string>
<string name="desktop_notification_title">Meshtastic</string>
<string name="filter_icon">필터</string>
</resources>

View file

@ -190,6 +190,7 @@
<string name="meshtastic_new_nodes_notifications">Naujo įtaiso pranešimas</string>
<string name="snr">SNR</string>
<string name="rssi">RSSI</string>
<string name="node_map">Įtaisų žemėlapis</string>
<string name="administration">Administravimas</string>
<string name="remote_admin">Nuotolinis administravimas</string>
<string name="bad">Silpnas</string>
@ -216,7 +217,6 @@
<string name="alert_bell_text">Skambučio simbolis!</string>
<string name="red">Raudona</string>
<string name="region_frequency_plan">Regionas</string>
<string name="mqtt_status_disconnected">Atsijungta</string>
<string name="public_key">Viešasis raktas</string>
<string name="private_key">Privatus raktas</string>
<string name="timeout">Baigėsi laikas</string>
@ -237,5 +237,4 @@
<!-- Message Filter -->
<!-- Network Map Layers -->
<string name="tak_team_red">Raudona</string>
<string name="filter_icon">Filtras</string>
</resources>

View file

@ -201,8 +201,11 @@
<string name="encryption_error">Publieke sleutel komt niet overeen</string>
<string name="meshtastic_new_nodes_notifications">Nieuwe node meldingen</string>
<string name="snr">SNR</string>
<string name="snr_definition">Signal-to-Noise Ratio, een meeting die wordt gebruikt in de communicatie om het niveau van een gewenst signaal tegenover achtergrondlawaai te kwantificeren. In Meshtastische en andere draadloze systemen geeft een hoger SNR een zuiverder signaal aan dat de betrouwbaarheid en kwaliteit van de gegevensoverdracht kan verbeteren.</string>
<string name="rssi">RSSI</string>
<string name="rssi_definition">Ontvangen Signal Sterkte Indicator, een meting gebruikt om het stroomniveau te bepalen dat de antenne ontvangt. Een hogere RSSI-waarde geeft een sterkere en stabielere verbinding aan.</string>
<string name="iaq_definition">(Binnenluchtkwaliteit) relatieve schaal IAQ waarde gemeten door Bosch BME680. Waarde tussen 0 en 500.</string>
<string name="node_map">Node Kaart</string>
<string name="position_log">Positie</string>
<string name="administration">Beheer</string>
<string name="remote_admin">Extern beheer</string>
@ -309,8 +312,6 @@
<string name="ignore_incoming">Inkomende negeren</string>
<string name="ignore_mqtt">Negeer MQTT</string>
<string name="mqtt_config">MQTT Configuratie</string>
<string name="mqtt_status_disconnected">Niet verbonden</string>
<string name="mqtt_status_connected">Verbonden</string>
<string name="mqtt_enabled">MQTT ingeschakeld</string>
<string name="address">Adres</string>
<string name="username">Gebruikersnaam</string>
@ -415,5 +416,4 @@
<string name="tak_team_blue">Blauw</string>
<string name="tak_team_green">Groen</string>
<string name="connect">Verbinding maken</string>
<string name="filter_icon">Filter</string>
</resources>

View file

@ -194,8 +194,11 @@
<string name="encryption_error">Direktemeldinger bruker den nye offentlige nøkkelinfrastrukturen for kryptering. Krever firmware versjon 2.5 eller høyere.</string>
<string name="meshtastic_new_nodes_notifications">Varsel om nye noder</string>
<string name="snr">SNR</string>
<string name="snr_definition">Signal-to-Noise Ratio, et mål som brukes i kommunikasjon for å sette nivået av et ønsket signal til bakgrunnstrøynivået. I Meshtastic og andre trådløse systemer tyder et høyere SNR på et klarere signal som kan forbedre påliteligheten og kvaliteten på dataoverføringen.</string>
<string name="rssi">RSSI</string>
<string name="rssi_definition">\"Received Signal Strength Indicator\", en måling som brukes til å bestemme strømnivået som mottas av antennen. Høyere RSSI verdi indikerer generelt en sterkere og mer stabil forbindelse.</string>
<string name="iaq_definition">(Innendørs luftkvalitet) relativ skala IAQ-verdi målt ved Bosch BME680. Verdi 0500.</string>
<string name="node_map">Nodekart</string>
<string name="administration">Administrasjon</string>
<string name="remote_admin">Fjernadministrasjon</string>
<string name="bad">Dårlig</string>
@ -219,7 +222,6 @@
<string name="copy">Kopier</string>
<string name="alert_bell_text">Varsel, bjellekarakter!</string>
<string name="region_frequency_plan">Region</string>
<string name="mqtt_status_disconnected">Frakoblet</string>
<string name="public_key">Offentlig nøkkel</string>
<string name="private_key">Privat nøkkel</string>
<string name="timeout">Tidsavbrudd</string>
@ -238,5 +240,4 @@
<!-- Compass -->
<!-- Message Filter -->
<!-- Network Map Layers -->
<string name="filter_icon">Filter</string>
</resources>

View file

@ -333,9 +333,12 @@
<string name="userinfo">Informacje o użytkowniku</string>
<string name="meshtastic_new_nodes_notifications">Powiadomienia o nowych węzłach</string>
<string name="snr">SNR:</string>
<string name="snr_definition">Współczynnik sygnału do szumu (Signal-to-Noise Ratio) - miara stosowana w komunikacji do określania poziomu pożądanego sygnału w stosunku do poziomu szumu tła. W Meshtastic i innych systemach bezprzewodowych wyższy współczynnik SNR oznacza czystszy sygnał, który może zwiększyć niezawodność i jakość transmisji danych.</string>
<string name="rssi">RSSI:</string>
<string name="rssi_definition">Received Signal Strength Indicator - miara używana do określenia poziomu mocy odbieranej przez antenę. Wyższa wartość RSSI zazwyczaj oznacza silniejsze i bardziej stabilne połączenie.</string>
<string name="iaq_definition">Jakość powietrza w pomieszczeniach (Indoor Air Quality) - wartość względna w skali IAQ mierzona czujnikiem BME680. Zakres wartości: 0500.</string>
<string name="device_metrics_log">Metryka urządzenia</string>
<string name="node_map">Ślad na mapie</string>
<string name="position_log">Pozycjonowanie</string>
<string name="last_position_update">Ostatnia aktualizacja lokalizacji</string>
<string name="env_metrics_log">Metryki środowiskowe</string>
@ -496,8 +499,6 @@
<string name="ignore_mqtt">Zignoruj MQTT</string>
<string name="ok_to_mqtt">Ok dla MQTT</string>
<string name="mqtt_config">Konfiguracja MQTT</string>
<string name="mqtt_status_disconnected">Rozłączono</string>
<string name="mqtt_status_connected">Połączony</string>
<string name="mqtt_enabled">Włącz MQTT</string>
<string name="address">Adres</string>
<string name="username">Nazwa użytkownika</string>
@ -748,6 +749,4 @@
<string name="traffic_management_enabled">Moduł Włączony</string>
<string name="connect">Połącz</string>
<string name="done">Wykonano</string>
<string name="desktop_notification_title">Meshtastic</string>
<string name="filter_icon">Filtr</string>
</resources>

View file

@ -230,8 +230,11 @@
<string name="encryption_error">Chave pública não confere</string>
<string name="meshtastic_new_nodes_notifications">Novas notificações de nó</string>
<string name="snr">SNR</string>
<string name="snr_definition">Relação sinal-para-ruído, uma medida utilizada nas comunicações para quantificar o nível de um sinal desejado para o nível de ruído de fundo. Na Meshtastic e outros sistemas sem fios, uma SNR maior indica um sinal mais claro que pode melhorar a confiabilidade e a qualidade da transmissão de dados.</string>
<string name="rssi">RSSI</string>
<string name="rssi_definition">Indicador de Força de Sinal Recebido, uma medida usada para determinar o nível de potência que está sendo recebida pela antena. Um valor maior de RSSI geralmente indica uma conexão mais forte e mais estável.</string>
<string name="iaq_definition">(Qualidade do ar interior) valor relativo da escala IAQ medido pelo Bosch BME680. Intervalo de Valor de 0500.</string>
<string name="node_map">Mapa do nó</string>
<string name="position_log">Posição</string>
<string name="last_position_update">Atualização da última posição</string>
<string name="administration">Administração</string>
@ -379,8 +382,6 @@
<string name="pa_fan_disabled">Ventilador do PA desativado</string>
<string name="ignore_mqtt">Ignorar MQTT</string>
<string name="mqtt_config">Configurações MQTT</string>
<string name="mqtt_status_disconnected">Desconectado</string>
<string name="mqtt_status_connected">Conectado</string>
<string name="mqtt_enabled">MQTT habilitado</string>
<string name="address">Endereço</string>
<string name="username">Nome de usuário</string>
@ -664,6 +665,4 @@
<string name="tak_team_blue">Azul</string>
<string name="tak_team_green">Verde</string>
<string name="done">Concluído</string>
<string name="desktop_notification_title">Meshtastic</string>
<string name="filter_icon">Filtro</string>
</resources>

View file

@ -219,8 +219,11 @@
<string name="encryption_error">Incompatibilidade de chave pública</string>
<string name="meshtastic_new_nodes_notifications">Notificações de novos nodes</string>
<string name="snr">SNR</string>
<string name="snr_definition">Relação sinal-para-ruído, uma medida utilizada nas comunicações para quantificar o nível de um sinal desejado com o nível de ruído de fundo. Em Meshtastic e outros sistemas sem fio. Quanto mais alta for a relação sinal-ruído, menor é o efeito do ruído de fundo sobre a deteção ou medição do sinal.</string>
<string name="rssi">RSSI</string>
<string name="rssi_definition">Indicador de Força de Sinal Recebido, uma medida usada para determinar o nível de energia que está a ser recebido pela antena. Um valor mais elevado de RSSI geralmente indica uma conexão mais forte e mais estável.</string>
<string name="iaq_definition">(Qualidade do ar interior) valor relativo da escala IAQ conforme medida por Bosch BME680. Entre 0500.</string>
<string name="node_map">Mapa de nodes</string>
<string name="position_log">Posição</string>
<string name="administration">Administração</string>
<string name="remote_admin">Administração Remota</string>
@ -363,8 +366,6 @@
<string name="ignore_incoming">Ignorar entrada</string>
<string name="ignore_mqtt">Ignorar MQTT</string>
<string name="mqtt_config">Configuração MQTT</string>
<string name="mqtt_status_disconnected">Desconectado</string>
<string name="mqtt_status_connected">Ligado</string>
<string name="mqtt_enabled">MQTT ativo</string>
<string name="address">Endereço</string>
<string name="username">Utilizador</string>
@ -514,6 +515,4 @@
<string name="tak_team_blue">Azul</string>
<string name="tak_team_green">Verde</string>
<string name="connect">Ligar</string>
<string name="desktop_notification_title">Nome do nó de alternativo</string>
<string name="filter_icon">Filtrar</string>
</resources>

Some files were not shown because too many files have changed in this diff Show more