Compare commits

..

38 commits

Author SHA1 Message Date
James Rich
f21d8af9ae
fix(transport): improve BLE / TCP / USB reconnect and handshake resilience (#5196)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-20 17:34:16 +00:00
James Rich
a90cb2d89e
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5195)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-04-20 17:32:58 +00:00
Copilot
7492a33cf8
Fix node-details remove action to preserve confirmation flow (#5192)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jamesarich <2199651+jamesarich@users.noreply.github.com>
Co-authored-by: James Rich <james.a.rich@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-20 15:59:20 +00:00
James Rich
2b47da3b61
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5193)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-04-20 07:40:08 -05:00
renovate[bot]
3322257cfd
chore(deps): update plugin com.gradle.develocity to v4.4.1 (#5194)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 11:47:09 +00:00
James Rich
99e7407a90
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5189)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-04-19 20:07:52 +00:00
renovate[bot]
9dd57725f2
chore(deps): update vico to v3.2.0-next.1 (#5191)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-19 12:31:11 -05:00
renovate[bot]
2c1984ace5
chore(deps): update fastlane to v2.233.0 (#5190)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-19 16:30:34 +00:00
James Rich
94856d257f
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5186)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-04-18 12:09:22 +00:00
James Rich
84fe24467f
fix(widget): drive updates via debounced state observer (#5185)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-18 04:11:32 +00:00
renovate[bot]
68a414b75b
chore(deps): update compose-multiplatform to v1.11.0-rc01 (#5184)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-17 22:00:34 -05:00
James Rich
4257e7b7e4
chore(deps): split androidx-compose version ref from CMP (#5183)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 21:41:36 -05:00
James Rich
14e86b90f1
feat(mqtt): adopt mqttastic-client-kmp 0.2.0 — disconnect reasons + Test Connection (#5181)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 21:33:55 -05:00
James Rich
ef0e159abb
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5177)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-04-17 21:20:58 -05:00
James Rich
61d7f6fef3
fix(deps): pin androidx-compose runtime-tracing/ui-test to CMP version (#5179)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 15:46:59 -05:00
James Rich
a273dc6623
Revert "diag(r8): disable minify for release builds (animation-freeze diagnostic)" (#5176)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 15:07:54 -05:00
James Rich
c866f60b59
diag(r8): disable minify for release builds (animation-freeze diagnostic) (#5174)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 18:36:33 +00:00
James Rich
10bc58d417
chore(strings): remove 4 unused string resources (#5173)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 17:36:32 +00:00
James Rich
dd74e501f3
fix(ui): finish accessibility roles and action labels for clickable surfaces (#5170)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 17:33:38 +00:00
James Rich
56cbc3670d
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5163)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-04-17 17:13:27 +00:00
James Rich
15a7c19b74
chore(r8): remove redundant keep rules covered by consumer rules (#5172)
Co-authored-by: GitHub Copilot CLI <223556219+Copilot@users.noreply.github.com>
2026-04-17 17:13:26 +00:00
James Rich
b979663e24
refactor: consolidate metric formatting through MetricFormatter (#5169)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 17:13:01 +00:00
James Rich
9f3fe865e3
test: migrate MigrationTest to runTest and add missing repository fakes (#5171)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 16:35:41 +00:00
James Rich
90f6e21a9c
fix(ui): stable LazyColumn keys, semantic roles, and content descriptions (#5168)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 16:24:18 +00:00
James Rich
cdeb1ac532
fix: redact MeshLog proto secrets and centralize Compose keep-rules (#5166)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 16:20:50 +00:00
James Rich
adfe3bfed1
refactor: use injected ioDispatcher and ApplicationCoroutineScope (#5167)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 16:18:45 +00:00
James Rich
a97f704300
feat(mqtt): migrate to MQTTastic-Client-KMP (#5165)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 15:19:08 +00:00
James Rich
df3b5365f9
fix(node): don't recreate Vico CartesianChartModelProducer on channel switch (#5160)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 02:40:17 +00:00
James Rich
a6a889430b
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5159)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-04-16 21:43:35 -05:00
renovate[bot]
65b885a073
chore(deps): update core/proto/src/main/proto digest to 4d5b500 (#5161)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 21:41:36 -05:00
James Rich
17e69c6d4c
chore: review-cleanup fleet (audit + fix + hardening) (#5158) 2026-04-17 00:02:59 +00:00
James Rich
872c566ef1
chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#5157)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-04-16 20:25:12 +00:00
renovate[bot]
3a2f2fc56b
chore(deps): update kotlin to v2.3.21-rc2 (#5155)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 16:33:25 +00:00
renovate[bot]
50896d455b
chore(deps): update dd.sdk.android to v3.9.0 (#5156)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 16:33:13 +00:00
James Rich
a580cd0467
chore(analytics): disable Datadog Compose action tracking (#5153)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-15 23:09:21 +00:00
James Rich
8e5d99410c
refactor(di): adopt @KoinApplication with startKoin<T>() compiler plugin API (#5152)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-15 22:52:59 +00:00
renovate[bot]
0f900fe7d7
chore(deps): update core/proto/src/main/proto digest to c9067da (#5151)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 13:12:53 -05:00
James Rich
9ac02cf851
fix(app): disable R8 optimization to fix Compose animation freeze (#5150)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-15 17:45:27 +00:00
189 changed files with 3625 additions and 939 deletions

12
.github/lsp.json vendored Normal file
View file

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

View file

@ -56,6 +56,15 @@
"changelogUrl": "https://github.com/meshtastic/protobufs/compare/{{currentDigest}}...{{newDigest}}", "changelogUrl": "https://github.com/meshtastic/protobufs/compare/{{currentDigest}}...{{newDigest}}",
"automerge": true "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", "description": "Restrict sensitive infrastructure to manual minor updates",
"matchUpdateTypes": [ "matchUpdateTypes": [

View file

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

1
.gitignore vendored
View file

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

295
.pr5167.diff Normal file
View file

@ -0,0 +1,295 @@
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,15 +12,26 @@ 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. 4. **Resolution:** Resolve app-layer wrappers via `koinViewModel()` or injected bindings within Compose navigation graphs.
### Anti-Patterns ### Anti-Patterns
- **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). - **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.
- **Default Parameters:** Do **not** expect Koin to inject default parameters automatically. The K2 plugin's `skipDefaultValues = true` behavior skips parameters with default Kotlin values. - **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) ### Koin Startup Pattern (K2 Compiler Plugin)
The project uses the **K2 Compiler Plugin** (`koin-compiler-plugin`, not KSP). The correct canonical startup for this path is: 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:
```kotlin ```kotlin
startKoin { modules(AppKoinModule().module()) } // 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()
}
``` ```
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. - `@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.
## Navigation 3 ## Navigation 3
@ -39,6 +50,7 @@ Do **not** use `@KoinApplication` — that annotation is part of the **KSP annot
## Reference Anchors ## Reference Anchors
- **App Startup / Koin Bootstrap:** `app/src/main/kotlin/org/meshtastic/app/MeshUtilApplication.kt` - **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` - **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` - **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` - **Desktop Nav Shell:** `desktop/src/main/kotlin/org/meshtastic/desktop/ui/DesktopMainScreen.kt`

View file

@ -0,0 +1,79 @@
# 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,6 +18,7 @@ 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/testing-ci/` - Validation commands, CI pipeline architecture, CI Gradle properties.
- `.skills/implement-feature/` - Step-by-step feature workflow. - `.skills/implement-feature/` - Step-by-step feature workflow.
- `.skills/code-review/` - PR validation checklist. - `.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. - **Active Status:** Read `docs/kmp-status.md` and `docs/roadmap.md` to understand the current KMP migration epoch.
</context_and_memory> </context_and_memory>
@ -73,6 +74,33 @@ 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. - **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> </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> <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. - **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. - **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. - **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. - **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). - **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.

View file

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

View file

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

View file

@ -1,65 +1,45 @@
# ============================================================================ # ============================================================================
# Meshtastic Android ProGuard / R8 rules for release minification # Meshtastic Android ProGuard / R8 rules for release minification
# ============================================================================ # ============================================================================
# Open-source project: obfuscation is disabled. We rely on tree-shaking and # Open-source project: obfuscation and optimization are disabled. We rely on
# code optimization for APK size reduction. # 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.
# ============================================================================ # ============================================================================
# ---- General ---------------------------------------------------------------- # ---- General ----------------------------------------------------------------
# Preserve line numbers for meaningful crash stack traces
-keepattributes SourceFile,LineNumberTable
# Open-source no need to obfuscate # Open-source no need to obfuscate
-dontobfuscate -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) # 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. # for auditing. Inspect this file after a release build to see what libraries inject.
-printconfiguration build/outputs/mapping/r8-merged-config.txt -printconfiguration build/outputs/mapping/r8-merged-config.txt
# ---- Networking (transitive references from Ktor) --------------------------- # ---- Networking (transitive references from Ktor on Android) ----------------
-dontwarn org.conscrypt.** -dontwarn org.conscrypt.**
-dontwarn org.bouncycastle.** -dontwarn org.bouncycastle.**
-dontwarn org.openjsse.** -dontwarn org.openjsse.**
# ---- Wire Protobuf ---------------------------------------------------------- # Compose runtime/ui/animation/foundation/material3 keep rules now live in
# config/proguard/shared-rules.pro so both Android (R8) and desktop (ProGuard)
# Wire-generated proto message classes (accessed via ADAPTER companion reflection) # get the same defence-in-depth coverage against CMP 1.11 optimizer folding.
-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,15 +861,9 @@ private fun CacheInfoDialog(mapView: MapView, onDismiss: () -> Unit) {
onDismiss = onDismiss, onDismiss = onDismiss,
negativeButton = { TextButton(onClick = { onDismiss() }) { Text(text = stringResource(Res.string.close)) } }, negativeButton = { TextButton(onClick = { onDismiss() }) { Text(text = stringResource(Res.string.close)) } },
) { ) {
Text( val capacityMb = (cacheCapacity / (1024 * 1024)).toLong()
modifier = Modifier.padding(16.dp), val usageMb = (currentCacheUsage / (1024 * 1024)).toLong()
text = Text(modifier = Modifier.padding(16.dp), text = stringResource(Res.string.map_cache_info, capacityMb, usageMb))
stringResource(
Res.string.map_cache_info,
cacheCapacity / (1024.0 * 1024.0),
currentCacheUsage / (1024.0 * 1024.0),
),
)
} }
} }

View file

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

View file

@ -59,6 +59,7 @@ import org.meshtastic.app.node.metrics.getTracerouteMapOverlayInsets
import org.meshtastic.app.ui.MainScreen import org.meshtastic.app.ui.MainScreen
import org.meshtastic.core.barcode.rememberBarcodeScanner import org.meshtastic.core.barcode.rememberBarcodeScanner
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI 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.nfc.NfcScannerEffect
import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.channel_invalid import org.meshtastic.core.resources.channel_invalid
@ -91,6 +92,8 @@ import org.meshtastic.feature.node.metrics.TracerouteMapScreen
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val model: UIViewModel by viewModel() 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 * 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. * itself as a LifecycleObserver in its init block.
@ -166,6 +169,16 @@ class MainActivity : ComponentActivity() {
handleIntent(intent) 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 @Composable
private fun AppCompositionLocals(content: @Composable () -> Unit) { private fun AppCompositionLocals(content: @Composable () -> Unit) {
CompositionLocalProvider( CompositionLocalProvider(
@ -257,6 +270,11 @@ class MainActivity : ComponentActivity() {
UsbManager.ACTION_USB_DEVICE_ATTACHED -> { UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
Logger.d { "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() showSettingsPage()
} }

View file

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

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2025-2026 Meshtastic LLC * Copyright (c) 2026 Meshtastic LLC
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -14,9 +14,13 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.meshtastic.feature.settings.navigation package org.meshtastic.app.di
import org.meshtastic.core.navigation.SettingsRoute import org.koin.core.annotation.KoinApplication
actual fun getAboutLibrariesJson(): String = /**
SettingsRoute::class.java.getResource("/aboutlibraries.json")?.readText() ?: "" * Root Koin bootstrap for Android. The K2 compiler plugin uses this to discover the full module graph when
* [org.koin.plugin.module.dsl.startKoin] is called with this type parameter.
*/
@KoinApplication(modules = [AppKoinModule::class])
object AndroidKoinApp

View file

@ -25,6 +25,7 @@ import androidx.work.WorkerParameters
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.HttpClientEngine
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import org.koin.plugin.module.dsl.koinApplication
import org.koin.test.verify.definition import org.koin.test.verify.definition
import org.koin.test.verify.injectedParameters import org.koin.test.verify.injectedParameters
import org.koin.test.verify.verify import org.koin.test.verify.verify
@ -60,4 +61,19 @@ 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,7 +54,6 @@ dependencies {
compileOnly(libs.kotlin.gradlePlugin) compileOnly(libs.kotlin.gradlePlugin)
compileOnly(libs.ksp.gradlePlugin) compileOnly(libs.ksp.gradlePlugin)
compileOnly(libs.androidx.room.gradlePlugin) compileOnly(libs.androidx.room.gradlePlugin)
compileOnly(libs.secrets.gradlePlugin)
compileOnly(libs.spotless.gradlePlugin) compileOnly(libs.spotless.gradlePlugin)
compileOnly(libs.test.retry.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.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.datadog.gradle.plugin.DdExtension import com.datadog.gradle.plugin.DdExtension
import com.datadog.gradle.plugin.InjectBuildIdToAssetsTask import com.datadog.gradle.plugin.InjectBuildIdToAssetsTask
import com.datadog.gradle.plugin.InstrumentationMode
import com.datadog.gradle.plugin.SdkCheckLevel import com.datadog.gradle.plugin.SdkCheckLevel
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
@ -110,7 +110,7 @@ class AnalyticsConventionPlugin : Plugin<Project> {
variants { variants {
register(variant.name) { register(variant.name) {
site = "US5" site = "US5"
composeInstrumentation = InstrumentationMode.AUTO
} }
} }
checkProjectDependencies = SdkCheckLevel.NONE checkProjectDependencies = SdkCheckLevel.NONE

View file

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

View file

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

View file

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

View file

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

View file

@ -77,7 +77,9 @@ internal fun Project.configureKotlinMultiplatform() {
// upgrades to the CMP-bundled version, triggering a "Skiko dependencies' // upgrades to the CMP-bundled version, triggering a "Skiko dependencies'
// versions are incompatible" warning from CMP's compatibility checker. // versions are incompatible" warning from CMP's compatibility checker.
// Force the version to match CMP so the checker sees a consistent graph. // Force the version to match CMP so the checker sees a consistent graph.
val skikoVersion = libs.version("skiko") // 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"
configurations.configureEach { configurations.configureEach {
resolutionStrategy.eachDependency { resolutionStrategy.eachDependency {
if (requested.group == "org.jetbrains.skiko") { if (requested.group == "org.jetbrains.skiko") {

View file

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

View file

@ -0,0 +1,166 @@
# ============================================================================
# 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,7 +26,9 @@ import com.juul.kable.UnmetRequirementException
/** /**
* Classification of a BLE-layer exception for the transport layer to act on. * Classification of a BLE-layer exception for the transport layer to act on.
* *
* @property isPermanent `true` if the condition won't resolve without user intervention (e.g. Bluetooth disabled). * @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 gattStatus the platform GATT status code when available (Android-specific). * @property gattStatus the platform GATT status code when available (Android-specific).
* @property message a human-readable description of the failure. * @property message a human-readable description of the failure.
*/ */
@ -50,6 +52,9 @@ fun Throwable.classifyBleException(): BleExceptionInfo? = when (this) {
is GattRequestRejectedException -> is GattRequestRejectedException ->
BleExceptionInfo(isPermanent = false, message = "GATT request rejected (busy)") BleExceptionInfo(isPermanent = false, message = "GATT request rejected (busy)")
is UnmetRequirementException -> is UnmetRequirementException ->
BleExceptionInfo(isPermanent = true, message = message ?: "Bluetooth LE unavailable") // 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")
else -> null else -> null
} }

View file

@ -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
}

View file

@ -95,3 +95,18 @@ inline fun <T, R> T.safeCatching(block: T.() -> R): Result<R> = try {
} catch (e: Exception) { } catch (e: Exception) {
Result.failure(e) 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,6 +23,7 @@ package org.meshtastic.core.common.util
* All methods return locale-independent strings using [NumberFormatter] (dot decimal separator), which is intentional * All methods return locale-independent strings using [NumberFormatter] (dot decimal separator), which is intentional
* for a mesh networking app where consistency matters. * for a mesh networking app where consistency matters.
*/ */
@Suppress("TooManyFunctions")
object MetricFormatter { object MetricFormatter {
fun temperature(celsius: Float, isFahrenheit: Boolean): String { fun temperature(celsius: Float, isFahrenheit: Boolean): String {
@ -47,6 +48,12 @@ object MetricFormatter {
fun snr(value: Float, decimalPlaces: Int = 1): String = "${NumberFormatter.format(value, decimalPlaces)} dB" fun snr(value: Float, decimalPlaces: Int = 1): String = "${NumberFormatter.format(value, decimalPlaces)} dB"
fun rssi(value: Int): String = "$value dBm" 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 private const val FAHRENHEIT_SCALE = 1.8f

View file

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

View file

@ -60,6 +60,7 @@ import org.meshtastic.proto.AdminMessage
import org.meshtastic.proto.Config import org.meshtastic.proto.Config
import org.meshtastic.proto.Telemetry import org.meshtastic.proto.Telemetry
import org.meshtastic.proto.ToRadio import org.meshtastic.proto.ToRadio
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
import kotlin.time.DurationUnit import kotlin.time.DurationUnit
@ -211,11 +212,11 @@ class MeshConnectionManagerImpl(
} }
} }
private fun startHandshakeStallGuard(stage: Int, action: () -> Unit) { private fun startHandshakeStallGuard(stage: Int, timeout: Duration, action: () -> Unit) {
handshakeTimeout?.cancel() handshakeTimeout?.cancel()
handshakeTimeout = handshakeTimeout =
scope.handledLaunch { scope.handledLaunch {
delay(HANDSHAKE_TIMEOUT) delay(timeout)
if (serviceRepository.connectionState.value is ConnectionState.Connecting) { if (serviceRepository.connectionState.value is ConnectionState.Connecting) {
// Attempt one retry. Note: the firmware silently drops identical consecutive // Attempt one retry. Note: the firmware silently drops identical consecutive
// writes (per-connection dedup). If the first want_config_id was received and // writes (per-connection dedup). If the first want_config_id was received and
@ -291,13 +292,13 @@ class MeshConnectionManagerImpl(
override fun startConfigOnly() { override fun startConfigOnly() {
val action = { packetHandler.sendToRadio(ToRadio(want_config_id = HandshakeConstants.CONFIG_NONCE)) } val action = { packetHandler.sendToRadio(ToRadio(want_config_id = HandshakeConstants.CONFIG_NONCE)) }
startHandshakeStallGuard(1, action) startHandshakeStallGuard(1, HANDSHAKE_TIMEOUT_STAGE1, action)
action() action()
} }
override fun startNodeInfoOnly() { override fun startNodeInfoOnly() {
val action = { packetHandler.sendToRadio(ToRadio(want_config_id = HandshakeConstants.NODE_INFO_NONCE)) } val action = { packetHandler.sendToRadio(ToRadio(want_config_id = HandshakeConstants.NODE_INFO_NONCE)) }
startHandshakeStallGuard(2, action) startHandshakeStallGuard(2, HANDSHAKE_TIMEOUT_STAGE2, action)
action() action()
} }
@ -404,7 +405,14 @@ class MeshConnectionManagerImpl(
*/ */
private const val PRE_HANDSHAKE_SETTLE_MS = 100L private const val PRE_HANDSHAKE_SETTLE_MS = 100L
private val HANDSHAKE_TIMEOUT = 30.seconds 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
// Shorter window for the retry attempt: if the device genuinely didn't receive the // 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 // first want_config_id the retry completes within a few seconds. Waiting another 30s

View file

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

View file

@ -20,15 +20,28 @@ import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity import co.touchlab.kermit.Severity
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job 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.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import org.koin.core.annotation.Named import org.koin.core.annotation.Named
import org.koin.core.annotation.Single 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.MQTTRepository
import org.meshtastic.core.network.repository.resolveEndpoint
import org.meshtastic.core.repository.MqttManager import org.meshtastic.core.repository.MqttManager
import org.meshtastic.core.repository.PacketHandler import org.meshtastic.core.repository.PacketHandler
import org.meshtastic.core.repository.ServiceRepository 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.MqttClientProxyMessage
import org.meshtastic.proto.ToRadio import org.meshtastic.proto.ToRadio
@ -40,18 +53,30 @@ class MqttManagerImpl(
@Named("ServiceScope") private val scope: CoroutineScope, @Named("ServiceScope") private val scope: CoroutineScope,
) : MqttManager { ) : MqttManager {
private var mqttMessageFlow: Job? = null 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) { override fun startProxy(enabled: Boolean, proxyToClientEnabled: Boolean) {
if (mqttMessageFlow?.isActive == true) return if (mqttMessageFlow?.isActive == true) return
if (enabled && proxyToClientEnabled) { if (enabled && proxyToClientEnabled) {
proxyActive.value = true
mqttMessageFlow = mqttMessageFlow =
mqttRepository.proxyMessageFlow mqttRepository.proxyMessageFlow
.onEach { message -> packetHandler.sendToRadio(ToRadio(mqttClientProxyMessage = message)) } .onEach { message -> packetHandler.sendToRadio(ToRadio(mqttClientProxyMessage = message)) }
.catch { throwable -> .catch { throwable ->
serviceRepository.setErrorMessage( proxyActive.value = false
text = "MqttClientProxy failed: $throwable", val message =
severity = Severity.Warn, 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)
} }
.launchIn(scope) .launchIn(scope)
} }
@ -63,6 +88,7 @@ class MqttManagerImpl(
mqttMessageFlow?.cancel() mqttMessageFlow?.cancel()
mqttMessageFlow = null mqttMessageFlow = null
} }
proxyActive.value = false
} }
override fun handleMqttProxyMessage(message: MqttClientProxyMessage) { override fun handleMqttProxyMessage(message: MqttClientProxyMessage) {
@ -79,4 +105,57 @@ class MqttManagerImpl(
else -> {} 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,7 +22,9 @@ import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
@ -73,6 +75,11 @@ class PacketHandlerImpl(
private val queueMutex = Mutex() private val queueMutex = Mutex()
private val queuedPackets = mutableListOf<MeshPacket>() 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() // Set to true by stopPacketQueue() under queueMutex. Checked by startPacketQueueLocked()
// and the queue processor's finally block to prevent restarting a stopped queue. // and the queue processor's finally block to prevent restarting a stopped queue.
private var queueStopped = false private var queueStopped = false
@ -80,6 +87,20 @@ class PacketHandlerImpl(
private val responseMutex = Mutex() private val responseMutex = Mutex()
private val queueResponse = mutableMapOf<Int, CompletableDeferred<Boolean>>() 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) { override fun sendToRadio(p: ToRadio) {
Logger.d { "Sending to radio ${p.toPIIString()}" } Logger.d { "Sending to radio ${p.toPIIString()}" }
val b = p.encode() val b = p.encode()
@ -104,13 +125,9 @@ class PacketHandlerImpl(
} }
override fun sendToRadio(packet: MeshPacket) { override fun sendToRadio(packet: MeshPacket) {
scope.launch { // Non-suspend entry point — order-preserving via unbounded channel drained by
queueMutex.withLock { // a single consumer coroutine. trySend on UNLIMITED never fails for capacity.
queueStopped = false // Allow queue to resume after a disconnect/reconnect cycle. outboundChannel.trySend(packet)
queuedPackets.add(packet)
startPacketQueueLocked()
}
}
} }
@Suppress("TooGenericExceptionCaught", "SwallowedException") @Suppress("TooGenericExceptionCaught", "SwallowedException")

View file

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

View file

@ -47,6 +47,7 @@ import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.repository.PlatformAnalytics import org.meshtastic.core.repository.PlatformAnalytics
import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.ServiceBroadcasts import org.meshtastic.core.repository.ServiceBroadcasts
import org.meshtastic.core.repository.UiPrefs
import org.meshtastic.proto.AdminMessage import org.meshtastic.proto.AdminMessage
import org.meshtastic.proto.Channel import org.meshtastic.proto.Channel
import org.meshtastic.proto.Config import org.meshtastic.proto.Config
@ -68,6 +69,7 @@ class MeshActionHandlerImplTest {
private val dataHandler = mock<MeshDataHandler>(MockMode.autofill) private val dataHandler = mock<MeshDataHandler>(MockMode.autofill)
private val analytics = mock<PlatformAnalytics>(MockMode.autofill) private val analytics = mock<PlatformAnalytics>(MockMode.autofill)
private val meshPrefs = mock<MeshPrefs>(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 databaseManager = mock<DatabaseManager>(MockMode.autofill)
private val notificationManager = mock<NotificationManager>(MockMode.autofill) private val notificationManager = mock<NotificationManager>(MockMode.autofill)
private val messageProcessor = mock<MeshMessageProcessor>(MockMode.autofill) private val messageProcessor = mock<MeshMessageProcessor>(MockMode.autofill)
@ -100,6 +102,7 @@ class MeshActionHandlerImplTest {
dataHandler = lazy { dataHandler }, dataHandler = lazy { dataHandler },
analytics = analytics, analytics = analytics,
meshPrefs = meshPrefs, meshPrefs = meshPrefs,
uiPrefs = uiPrefs,
databaseManager = databaseManager, databaseManager = databaseManager,
notificationManager = notificationManager, notificationManager = notificationManager,
messageProcessor = lazy { messageProcessor }, messageProcessor = lazy { messageProcessor },
@ -356,7 +359,7 @@ class MeshActionHandlerImplTest {
@Test @Test
fun handleRequestPosition_provideLocation_validPosition_usesGivenPosition() { fun handleRequestPosition_provideLocation_validPosition_usesGivenPosition() {
handler = createHandler(testScope) handler = createHandler(testScope)
every { meshPrefs.shouldProvideNodeLocation(MY_NODE_NUM) } returns MutableStateFlow(true) every { uiPrefs.shouldProvideNodeLocation(MY_NODE_NUM) } returns MutableStateFlow(true)
val validPosition = Position(37.7749, -122.4194, 10) val validPosition = Position(37.7749, -122.4194, 10)
handler.handleRequestPosition(REMOTE_NODE_NUM, validPosition, MY_NODE_NUM) handler.handleRequestPosition(REMOTE_NODE_NUM, validPosition, MY_NODE_NUM)
@ -367,7 +370,7 @@ class MeshActionHandlerImplTest {
@Test @Test
fun handleRequestPosition_provideLocation_invalidPosition_fallsBackToNodeDB() { fun handleRequestPosition_provideLocation_invalidPosition_fallsBackToNodeDB() {
handler = createHandler(testScope) handler = createHandler(testScope)
every { meshPrefs.shouldProvideNodeLocation(MY_NODE_NUM) } returns MutableStateFlow(true) every { uiPrefs.shouldProvideNodeLocation(MY_NODE_NUM) } returns MutableStateFlow(true)
every { nodeManager.nodeDBbyNodeNum } returns emptyMap() every { nodeManager.nodeDBbyNodeNum } returns emptyMap()
val invalidPosition = Position(0.0, 0.0, 0) val invalidPosition = Position(0.0, 0.0, 0)
@ -380,7 +383,7 @@ class MeshActionHandlerImplTest {
@Test @Test
fun handleRequestPosition_doNotProvide_sendsZeroPosition() { fun handleRequestPosition_doNotProvide_sendsZeroPosition() {
handler = createHandler(testScope) handler = createHandler(testScope)
every { meshPrefs.shouldProvideNodeLocation(MY_NODE_NUM) } returns MutableStateFlow(false) every { uiPrefs.shouldProvideNodeLocation(MY_NODE_NUM) } returns MutableStateFlow(false)
val validPosition = Position(37.7749, -122.4194, 10) val validPosition = Position(37.7749, -122.4194, 10)
handler.handleRequestPosition(REMOTE_NODE_NUM, validPosition, MY_NODE_NUM) 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.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest
import okio.ByteString.Companion.toByteString import okio.ByteString.Companion.toByteString
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
@ -59,7 +59,7 @@ class MigrationTest {
) )
@Before @Before
fun createDb(): Unit = runBlocking { fun createDb(): Unit = runTest {
val context = ApplicationProvider.getApplicationContext<android.content.Context>() val context = ApplicationProvider.getApplicationContext<android.content.Context>()
database = database =
Room.inMemoryDatabaseBuilder<MeshtasticDatabase>( Room.inMemoryDatabaseBuilder<MeshtasticDatabase>(
@ -77,7 +77,7 @@ class MigrationTest {
} }
@Test @Test
fun testMigrateChannelsByPSK_duplicatePSK() = runBlocking { fun testMigrateChannelsByPSK_duplicatePSK() = runTest {
// PSK \"AQ==\" is base64 for single byte 0x01 // PSK \"AQ==\" is base64 for single byte 0x01
val pskBytes = byteArrayOf(0x01).toByteString() val pskBytes = byteArrayOf(0x01).toByteString()
@ -103,7 +103,7 @@ class MigrationTest {
} }
@Test @Test
fun testMigrateChannelsByPSK_reorder() = runBlocking { fun testMigrateChannelsByPSK_reorder() = runTest {
val pskA = byteArrayOf(0x01).toByteString() val pskA = byteArrayOf(0x01).toByteString()
val pskB = byteArrayOf(0x02).toByteString() val pskB = byteArrayOf(0x02).toByteString()
@ -122,7 +122,7 @@ class MigrationTest {
} }
@Test @Test
fun testMigrateChannelsByPSK_disambiguateByName() = runBlocking { fun testMigrateChannelsByPSK_disambiguateByName() = runTest {
val pskA = byteArrayOf(0x01).toByteString() val pskA = byteArrayOf(0x01).toByteString()
insertPacket(channel = 0, text = "Msg A1") insertPacket(channel = 0, text = "Msg A1")
@ -141,7 +141,7 @@ class MigrationTest {
} }
@Test @Test
fun testMigrateChannelsByPSK_preferSameIndexIfStillAmbiguous() = runBlocking { fun testMigrateChannelsByPSK_preferSameIndexIfStillAmbiguous() = runTest {
val pskA = byteArrayOf(0x01).toByteString() val pskA = byteArrayOf(0x01).toByteString()
insertPacket(channel = 0, text = "Msg A") insertPacket(channel = 0, text = "Msg A")

View file

@ -271,6 +271,42 @@ abstract class CommonPacketDaoTest {
assertFalse(excludingFiltered.any { it.packet.filtered }) 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 { companion object {
private const val SAMPLE_SIZE = 10 private const val SAMPLE_SIZE = 10
} }

View file

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

View file

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

View file

@ -0,0 +1,56 @@
/*
* 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

@ -0,0 +1,52 @@
/*
* 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,8 +18,11 @@
package org.meshtastic.core.model.util package org.meshtastic.core.model.util
import org.meshtastic.proto.Channel
import org.meshtastic.proto.Config import org.meshtastic.proto.Config
import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.ModuleConfig
import org.meshtastic.proto.MyNodeInfo
import org.meshtastic.proto.Telemetry import org.meshtastic.proto.Telemetry
/** /**
@ -48,6 +51,24 @@ fun MeshPacket.toOneLineString(): String {
return this.toString().replace(redactedFields.toRegex()) { "${it.groupValues[1]}=[REDACTED]" }.replace('\n', ' ') 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) { fun Any.toPIIString() = if (!isDebug) {
"<PII?>" "<PII?>"
} else { } else {

View file

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

View file

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

View file

@ -108,7 +108,10 @@ class SerialRadioTransport(
"Uptime: ${uptime}ms, " + "Uptime: ${uptime}ms, " +
"Packets RX: $packetsReceived ($bytesReceived bytes)" "Packets RX: $packetsReceived ($bytesReceived bytes)"
} }
onDeviceDisconnect(false) // 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)
} }
}, },
) )

View file

@ -87,6 +87,11 @@ internal class SerialConnectionImpl(
port.open(usbDeviceConnection) port.open(usbDeviceConnection)
port.setParameters(115200, UsbSerialPort.DATABITS_8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE) 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.dtr = true
port.rts = true port.rts = true

View file

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

View file

@ -22,9 +22,8 @@ import co.touchlab.kermit.Logger
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
@ -37,6 +36,7 @@ import kotlinx.coroutines.job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import org.meshtastic.core.ble.BleConnection import org.meshtastic.core.ble.BleConnection
import org.meshtastic.core.ble.BleConnectionFactory import org.meshtastic.core.ble.BleConnectionFactory
@ -133,7 +133,11 @@ class BleRadioTransport(
@Volatile private var isFullyConnected = false @Volatile private var isFullyConnected = false
private var connectionJob: Job? = null private var connectionJob: Job? = null
private val reconnectPolicy = BleReconnectPolicy()
// 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 heartbeatSender = private val heartbeatSender =
HeartbeatSender( HeartbeatSender(
@ -396,14 +400,14 @@ class BleRadioTransport(
} }
/** Closes the connection to the device. */ /** Closes the connection to the device. */
override fun close() { override suspend fun close() {
Logger.i { "[$address] Disconnecting. ${formatSessionStats()}" } Logger.i { "[$address] Disconnecting. ${formatSessionStats()}" }
connectionScope.cancel("close() called") connectionScope.cancel("close() called")
// GATT cleanup must outlive scope cancellation — GlobalScope is intentional. // GATT cleanup must run under NonCancellable so a cancelled caller cannot skip it,
// SharedRadioInterfaceService cancels the scope immediately after close(), so a // which would leak BluetoothGatt and trigger status 133 on the next reconnect.
// coroutine launched there may never run, leaking BluetoothGatt (causes GATT 133). // Using withContext (not runBlocking) keeps the caller's thread free — this is
@OptIn(DelicateCoroutinesApi::class) // critical when close() is invoked from the main thread during process shutdown.
GlobalScope.launch { withContext(NonCancellable) {
try { try {
withTimeoutOrNull(GATT_CLEANUP_TIMEOUT) { bleConnection.disconnect() } withTimeoutOrNull(GATT_CLEANUP_TIMEOUT) { bleConnection.disconnect() }
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) { } catch (@Suppress("TooGenericExceptionCaught") e: Exception) {

View file

@ -26,10 +26,11 @@ import kotlin.time.Duration.Companion.seconds
/** /**
* Encapsulates the BLE reconnection policy with exponential backoff. * Encapsulates the BLE reconnection policy with exponential backoff.
* *
* The policy tracks consecutive failures and decides whether to retry, signal a transient disconnect (DeviceSleep), or * The policy tracks consecutive failures and decides whether to retry or signal a transient disconnect (DeviceSleep).
* give up permanently. * 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.
* *
* @param maxFailures maximum consecutive failures before giving up permanently * @param maxFailures maximum consecutive failures before giving up; use [Int.MAX_VALUE] to retry indefinitely
* @param failureThreshold after this many consecutive failures, signal a transient disconnect * @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 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" * @param minStableConnection minimum time a connection must stay up to be considered "stable"
@ -148,7 +149,18 @@ class BleReconnectPolicy(
companion object { companion object {
const val DEFAULT_MAX_FAILURES = 10 const val DEFAULT_MAX_FAILURES = 10
const val DEFAULT_FAILURE_THRESHOLD = 3 const val DEFAULT_FAILURE_THRESHOLD = 3
val DEFAULT_SETTLE_DELAY = 1.seconds
/**
* 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_MIN_STABLE_CONNECTION = 5.seconds val DEFAULT_MIN_STABLE_CONNECTION = 5.seconds
internal val RECONNECT_BASE_DELAY = 5.seconds internal val RECONNECT_BASE_DELAY = 5.seconds

View file

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

View file

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

View file

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

View file

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

View file

@ -17,22 +17,15 @@
package org.meshtastic.core.network.repository package org.meshtastic.core.network.repository
import co.touchlab.kermit.Logger 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.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.ProducerScope
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow 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.callbackFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -44,11 +37,19 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonDecodingException import kotlinx.serialization.json.JsonDecodingException
import okio.ByteString.Companion.toByteString import okio.ByteString.Companion.toByteString
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.safeCatching
import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.model.MqttJsonPayload import org.meshtastic.core.model.MqttJsonPayload
import org.meshtastic.core.model.util.subscribeList import org.meshtastic.core.model.util.subscribeList
import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.RadioConfigRepository 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 org.meshtastic.proto.MqttClientProxyMessage
import kotlin.concurrent.Volatile import kotlin.concurrent.Volatile
@ -64,12 +65,16 @@ class MQTTRepositoryImpl(
private const val DEFAULT_TOPIC_LEVEL = "/2/e/" private const val DEFAULT_TOPIC_LEVEL = "/2/e/"
private const val JSON_TOPIC_LEVEL = "/2/json/" private const val JSON_TOPIC_LEVEL = "/2/json/"
private const val DEFAULT_SERVER_ADDRESS = "mqtt.meshtastic.org" 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 INITIAL_RECONNECT_DELAY_MS = 1000L
private const val MAX_RECONNECT_DELAY_MS = 30_000L private const val MAX_RECONNECT_DELAY_MS = 30_000L
private const val RECONNECT_BACKOFF_MULTIPLIER = 2 private const val RECONNECT_BACKOFF_MULTIPLIER = 2
} }
@Volatile private var client: MQTTClient? = null @Volatile private var client: MqttClient? = null
private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected.Idle)
override val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
private val json = Json { private val json = Json {
@ -77,25 +82,17 @@ class MQTTRepositoryImpl(
exceptionsWithDebugInfo = false exceptionsWithDebugInfo = false
} }
private val scope = CoroutineScope(dispatchers.default + SupervisorJob()) private val scope = CoroutineScope(dispatchers.default + SupervisorJob())
@Volatile private var clientJob: Job? = null
private val publishSemaphore = Semaphore(20) private val publishSemaphore = Semaphore(20)
@Suppress("TooGenericExceptionCaught")
override fun disconnect() { override fun disconnect() {
Logger.i { "MQTT Disconnecting" } Logger.i { "MQTT Disconnecting" }
val c = client val c = client
client = null // Null first to prevent re-entrant disconnect client = null
try { _connectionState.value = ConnectionState.Disconnected.Idle
c?.disconnect(ReasonCode.SUCCESS) scope.launch { safeCatching { c?.close() }.onFailure { e -> Logger.w(e) { "MQTT clean disconnect failed" } } }
} catch (e: Exception) {
Logger.w(e) { "MQTT clean disconnect failed" }
}
clientJob?.cancel()
clientJob = null
} }
@OptIn(ExperimentalUnsignedTypes::class) @OptIn(ExperimentalSerializationApi::class)
override val proxyMessageFlow: Flow<MqttClientProxyMessage> = callbackFlow { override val proxyMessageFlow: Flow<MqttClientProxyMessage> = callbackFlow {
val ownerId = "MeshtasticAndroidMqttProxy-${nodeRepository.myId.value ?: "unknown"}" val ownerId = "MeshtasticAndroidMqttProxy-${nodeRepository.myId.value ?: "unknown"}"
val channelSet = radioConfigRepository.channelSetFlow.first() val channelSet = radioConfigRepository.channelSetFlow.first()
@ -103,108 +100,105 @@ class MQTTRepositoryImpl(
val rootTopic = mqttConfig?.root?.ifEmpty { DEFAULT_TOPIC_ROOT } ?: DEFAULT_TOPIC_ROOT val rootTopic = mqttConfig?.root?.ifEmpty { DEFAULT_TOPIC_ROOT } ?: DEFAULT_TOPIC_ROOT
val (host, port) = val rawAddress = mqttConfig?.address ?: DEFAULT_SERVER_ADDRESS
(mqttConfig?.address ?: DEFAULT_SERVER_ADDRESS).split(":", limit = 2).let { val endpoint = resolveEndpoint(rawAddress, mqttConfig?.tls_enabled == true)
it[0] to (it.getOrNull(1)?.toIntOrNull() ?: if (mqttConfig?.tls_enabled == true) 8883 else 1883)
}
val newClient = val newClient =
MQTTClient( MqttClient(ownerId) {
mqttVersion = MQTTVersion.MQTT5, keepAliveSeconds = KEEPALIVE_SECONDS
address = host, autoReconnect = true
port = port, username = mqttConfig?.username
tls = if (mqttConfig?.tls_enabled == true) TLSClientSettings() else null, mqttConfig?.password?.let { password(it) }
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 client = newClient
// Subscribe before starting the event loop. KMQTT's subscribe() calls send(), val subscriptions: List<Subscription> = buildList {
// which queues the SUBSCRIBE packet in pendingSendMessages while connackReceived channelSet.subscribeList.forEach { globalId ->
// is false. Once the event loop receives CONNACK, it flushes the queue — so add(
// subscriptions are guaranteed to be sent immediately after the connection is Subscription(
// established, with no timing races. This replaces a previous yield()-based "$rootTopic$DEFAULT_TOPIC_LEVEL$globalId/+",
// approach that was unreliable on lightly loaded dispatchers. maxQos = QoS.AT_LEAST_ONCE,
val subscriptions = mutableListOf<Subscription>() noLocal = true,
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(
subscriptions.add(Subscription("$rootTopic${DEFAULT_TOPIC_LEVEL}PKI/+", SubscriptionOptions(Qos.AT_LEAST_ONCE))) Subscription(
"$rootTopic$JSON_TOPIC_LEVEL$globalId/+",
if (subscriptions.isNotEmpty()) { maxQos = QoS.AT_LEAST_ONCE,
Logger.d { "MQTT subscribing to ${subscriptions.size} topics" } noLocal = true,
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)
} }
} }
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)
}
}
}
}
awaitClose { disconnect() } awaitClose { disconnect() }
} }
@OptIn(ExperimentalUnsignedTypes::class) @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))
}
}
override fun publish(topic: String, data: ByteArray, retained: Boolean) { override fun publish(topic: String, data: ByteArray, retained: Boolean) {
val currentClient = client val currentClient = client
if (currentClient == null) { if (currentClient == null) {
@ -214,18 +208,36 @@ class MQTTRepositoryImpl(
Logger.d { "MQTT publishing message to topic $topic (size: ${data.size} bytes, retained: $retained)" } Logger.d { "MQTT publishing message to topic $topic (size: ${data.size} bytes, retained: $retained)" }
scope.launch { scope.launch {
publishSemaphore.withPermit { publishSemaphore.withPermit {
@Suppress("TooGenericExceptionCaught") safeCatching {
try {
currentClient.publish( currentClient.publish(
retain = retained, MqttMessage(topic = topic, payload = data, qos = QoS.AT_LEAST_ONCE, 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,6 +22,7 @@ import dev.mokkery.every
import dev.mokkery.matcher.any import dev.mokkery.matcher.any
import dev.mokkery.mock import dev.mokkery.mock
import dev.mokkery.verify import dev.mokkery.verify
import dev.mokkery.verify.VerifyMode
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.advanceTimeBy
@ -95,10 +96,10 @@ class BleRadioTransportTest {
* [RadioInterfaceService.onDisconnect] must be called so the higher layers can react (e.g. start the device-sleep * [RadioInterfaceService.onDisconnect] must be called so the higher layers can react (e.g. start the device-sleep
* timeout in [MeshConnectionManagerImpl]). * timeout in [MeshConnectionManagerImpl]).
* *
* Virtual-time breakdown (DEFAULT_FAILURE_THRESHOLD = 3): t = 1 000 ms iteration 1 settle delay elapses, * Virtual-time breakdown (DEFAULT_FAILURE_THRESHOLD = 3, DEFAULT_SETTLE_DELAY = 3 s): t = 3 000 ms iteration 1
* connectAndAwait throws, backoff 5 s starts t = 6 000 ms backoff ends t = 7 000 ms iteration 2 settle delay * settle delay elapses, connectAndAwait throws, backoff 5 s starts t = 8 000 ms backoff ends t = 11 000 ms
* elapses, connectAndAwait throws, backoff 10 s starts t = 17 000 ms backoff ends t = 18 000 ms iteration 3 * iteration 2 settle delay elapses, connectAndAwait throws, backoff 10 s starts t = 21 000 ms backoff ends t = 24
* settle delay elapses, connectAndAwait throws onDisconnect called * 000 ms iteration 3 settle delay elapses, connectAndAwait throws onDisconnect called
*/ */
@Test @Test
fun `onDisconnect is called after DEFAULT_FAILURE_THRESHOLD consecutive failures`() = runTest { fun `onDisconnect is called after DEFAULT_FAILURE_THRESHOLD consecutive failures`() = runTest {
@ -119,10 +120,10 @@ class BleRadioTransportTest {
) )
bleTransport.start() bleTransport.start()
// Advance through exactly 3 failure iterations (≈18 001 ms virtual time). // Advance through exactly 3 failure iterations (≈24 001 ms virtual time).
// The 4th iteration's backoff hasn't elapsed yet, so the coroutine is suspended // The 4th iteration's backoff hasn't elapsed yet, so the coroutine is suspended
// and advanceTimeBy returns cleanly. // and advanceTimeBy returns cleanly.
advanceTimeBy(18_001L) advanceTimeBy(24_001L)
verify { service.onDisconnect(any(), any()) } verify { service.onDisconnect(any(), any()) }
@ -131,16 +132,17 @@ class BleRadioTransportTest {
} }
/** /**
* After [BleReconnectPolicy.DEFAULT_MAX_FAILURES] (10) consecutive failures, the reconnect loop should stop and * Reconnect policy must NEVER give up on its own. The transport is only ever instantiated for the user-selected
* signal a permanent disconnect. This prevents infinite battery drain when the device is genuinely offline. * 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.
* *
* Time budget for 10 failures with bonded device (no scan): Each iteration = 1s settle + connectAndAwait throw + * Time budget for 15 failures with bonded device (no scan): each iteration 3 s settle + immediate throw +
* backoff Backoffs: 5s, 10s, 20s, 40s, 60s, 60s, 60s, 60s, 60s, (exit at failure 10 before backoff) Total 10×1s * 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 + 5+10+20+40+60+60+60+60+60 = 10 + 375 = 385s 385_000ms We use a generous 400_000ms to cover any timing * settle = 45 s, total 780 s. Use 800_000 ms to cover variance.
* variance.
*/ */
@Test @Test
fun `reconnect loop stops after DEFAULT_MAX_FAILURES with permanent disconnect`() = runTest { fun `reconnect loop never gives up - no permanent disconnect from policy`() = runTest {
val device = FakeBleDevice(address = address, name = "Test Device") val device = FakeBleDevice(address = address, name = "Test Device")
bluetoothRepository.bond(device) bluetoothRepository.bond(device)
@ -158,11 +160,13 @@ class BleRadioTransportTest {
) )
bleTransport.start() bleTransport.start()
// Advance enough time for all 10 failures to occur. // Run well past where the legacy policy (maxFailures = 10) would have given up.
advanceTimeBy(400_001L) advanceTimeBy(800_001L)
// Should have been called with isPermanent=true at least once (the final call). // Transient disconnects (isPermanent = false) are expected once the failure threshold is hit;
verify { service.onDisconnect(isPermanent = true, errorMessage = any()) } // 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()) }
bleTransport.close() bleTransport.close()
} }

View file

@ -18,25 +18,82 @@ package org.meshtastic.core.network.repository
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.meshtastic.core.model.MqttJsonPayload import org.meshtastic.core.model.MqttJsonPayload
import org.meshtastic.mqtt.MqttEndpoint
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertTrue import kotlin.test.assertTrue
class MQTTRepositoryImplTest { class MQTTRepositoryImplTest {
@Test // region resolveEndpoint — every behavioral branch of address parsing.
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 address2 = "mqtt.example.com" @Test
val (host2, port2) = address2.split(":", limit = 2).let { it[0] to (it.getOrNull(1)?.toIntOrNull() ?: 1883) } fun `bare host without scheme is wrapped as ws WebSocket on the standard port`() {
assertEquals("mqtt.example.com", host2) val endpoint = resolveEndpoint(rawAddress = "broker.example.com", tlsEnabled = false)
assertEquals(1883, port2)
val ws = assertIs<MqttEndpoint.WebSocket>(endpoint)
assertEquals("ws://broker.example.com/mqtt", ws.url)
} }
@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 @Test
fun `test json payload parsing`() { fun `test json payload parsing`() {
val jsonStr = val jsonStr =
@ -72,4 +129,6 @@ class MQTTRepositoryImplTest {
assertTrue(jsonStr.contains("\"from\":12345678")) assertTrue(jsonStr.contains("\"from\":12345678"))
assertTrue(jsonStr.contains("\"payload\":\"Hello World\"")) assertTrue(jsonStr.contains("\"payload\":\"Hello World\""))
} }
// endregion
} }

View file

@ -74,11 +74,15 @@ open class TcpRadioTransport(
transport.start(address) transport.start(address)
} }
override fun close() { override suspend fun close() {
Logger.d { "[$address] Closing TCP transport" } Logger.d { "[$address] Closing TCP transport" }
closing = true closing = true
transport.stop() transport.stop()
callback.onDisconnect(isPermanent = true) // 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.
} }
override fun keepAlive() { override fun keepAlive() {

View file

@ -129,7 +129,10 @@ private constructor(
// Ignore errors during port close // Ignore errors during port close
} }
if (isActive) { if (isActive) {
onDeviceDisconnect(true) // 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)
} }
} }
} }
@ -154,7 +157,7 @@ private constructor(
serialPort = null serialPort = null
} }
override fun close() { override suspend fun close() {
Logger.d { "[$portName] Closing serial transport" } Logger.d { "[$portName] Closing serial transport" }
readJob?.cancel() readJob?.cancel()
readJob = null readJob = null
@ -169,8 +172,10 @@ private constructor(
private const val READ_TIMEOUT_MS = 100 private const val READ_TIMEOUT_MS = 100
/** /**
* Creates and opens a [SerialTransport]. If the port cannot be opened, the transport signals a permanent * 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. * 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.
*/ */
fun open( fun open(
portName: String, portName: String,
@ -183,7 +188,7 @@ private constructor(
if (!transport.startConnection()) { if (!transport.startConnection()) {
val errorMessage = diagnoseOpenFailure(portName) val errorMessage = diagnoseOpenFailure(portName)
Logger.w { "[$portName] Serial port could not be opened; signalling disconnect. $errorMessage" } Logger.w { "[$portName] Serial port could not be opened; signalling disconnect. $errorMessage" }
callback.onDisconnect(isPermanent = true, errorMessage = errorMessage) callback.onDisconnect(isPermanent = false, errorMessage = errorMessage)
} }
return transport return transport
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -1,43 +0,0 @@
# 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 940ac382a7d143040da5a880237f84c48ee31f2b Subproject commit 4d5b500df5af68a4f57d3e19705cc3bb1136358c

View file

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

View file

@ -16,10 +16,16 @@
*/ */
package org.meshtastic.core.repository 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 import org.meshtastic.proto.MqttClientProxyMessage
/** Interface for managing MQTT proxy communication. */ /** Interface for managing MQTT proxy communication. */
interface MqttManager { interface MqttManager {
/** Observable MQTT proxy connection state for UI consumption. */
val mqttConnectionState: StateFlow<MqttConnectionState>
/** Starts the MQTT proxy with the given settings. */ /** Starts the MQTT proxy with the given settings. */
fun startProxy(enabled: Boolean, proxyToClientEnabled: Boolean) fun startProxy(enabled: Boolean, proxyToClientEnabled: Boolean)
@ -28,4 +34,15 @@ interface MqttManager {
/** Handles an MQTT proxy message from the radio. */ /** Handles an MQTT proxy message from the radio. */
fun handleMqttProxyMessage(message: MqttClientProxyMessage) 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,6 +17,7 @@
package org.meshtastic.core.repository package org.meshtastic.core.repository
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.ConnectionState
@ -68,12 +69,26 @@ interface RadioInterfaceService : RadioTransportCallback {
/** Whether we are currently using a mock transport. */ /** Whether we are currently using a mock transport. */
fun isMockTransport(): Boolean fun isMockTransport(): Boolean
/** Flow of raw data received from the radio. */ /**
val receivedData: SharedFlow<ByteArray> * 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 radio activity events. */ /** Flow of radio activity events. */
val meshActivity: SharedFlow<MeshActivity> 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. */ /** Sends a raw byte array to the radio. */
fun sendToRadio(bytes: ByteArray) fun sendToRadio(bytes: ByteArray)

View file

@ -16,13 +16,11 @@
*/ */
package org.meshtastic.core.repository package org.meshtastic.core.repository
import okio.Closeable
/** /**
* Interface for hardware transports (BLE, Serial, TCP, etc.) that handles raw byte communication. This is the * 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. * KMP-compatible replacement for the legacy Android-specific IRadioInterface.
*/ */
interface RadioTransport : Closeable { interface RadioTransport {
/** Sends a raw byte array to the radio hardware. */ /** Sends a raw byte array to the radio hardware. */
fun handleSendToRadio(p: ByteArray) fun handleSendToRadio(p: ByteArray)
@ -39,4 +37,13 @@ interface RadioTransport : Closeable {
* function can be implemented by transports to see if we are really connected. * function can be implemented by transports to see if we are really connected.
*/ */
fun keepAlive() {} 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,13 +16,14 @@
*/ */
package org.meshtastic.core.repository package org.meshtastic.core.repository
import kotlinx.coroutines.test.runTest
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertTrue import kotlin.test.assertTrue
class RadioTransportTest { class RadioTransportTest {
@Test @Test
fun `RadioTransport can be implemented`() { fun `RadioTransport can be implemented`() = runTest {
var sentData: ByteArray? = null var sentData: ByteArray? = null
var closed = false var closed = false
var keepAliveCalled = false var keepAliveCalled = false
@ -37,7 +38,7 @@ class RadioTransportTest {
keepAliveCalled = true keepAliveCalled = true
} }
override fun close() { override suspend fun close() {
closed = true closed = true
} }
} }

View file

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

View file

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

View file

@ -201,10 +201,15 @@
<string name="reset_to_defaults">Възстановяване на настройките по подразбиране</string> <string name="reset_to_defaults">Възстановяване на настройките по подразбиране</string>
<string name="apply">Приложи</string> <string name="apply">Приложи</string>
<string name="theme">Тема</string> <string name="theme">Тема</string>
<string name="contrast">Контраст</string>
<string name="theme_light">Светла</string> <string name="theme_light">Светла</string>
<string name="theme_dark">Тъмна</string> <string name="theme_dark">Тъмна</string>
<string name="theme_system">По подразбиране на системата</string> <string name="theme_system">По подразбиране на системата</string>
<string name="choose_theme">Избор на тема</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="provide_location_to_mesh">Изпращане на местоположение в мрежата</string>
<string name="use_homoglyph_characters_encoding">Компактно кодиране за Кирилица</string> <string name="use_homoglyph_characters_encoding">Компактно кодиране за Кирилица</string>
<plurals name="delete_messages"> <plurals name="delete_messages">
@ -301,6 +306,8 @@
<string name="battery">Батерия</string> <string name="battery">Батерия</string>
<string name="channel_utilization">Използване на канала</string> <string name="channel_utilization">Използване на канала</string>
<string name="air_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_numeric_value">%1$s</string>
<string name="device_metrics_label_value">%1$s: %2$s</string> <string name="device_metrics_label_value">%1$s: %2$s</string>
<string name="logs">записа</string> <string name="logs">записа</string>
@ -317,12 +324,9 @@
<string name="encryption_error_text">Публичният ключ не съвпада със записания ключ. Можете да премахнете възела и да го оставите да обмени ключове отново, но това може да показва по-сериозен проблем със сигурността. Свържете се с потребителя чрез друг надежден канал, за да определите дали промяната на ключа се дължи на фабрично нулиране или друго умишлено действие.</string> <string name="encryption_error_text">Публичният ключ не съвпада със записания ключ. Можете да премахнете възела и да го оставите да обмени ключове отново, но това може да показва по-сериозен проблем със сигурността. Свържете се с потребителя чрез друг надежден канал, за да определите дали промяната на ключа се дължи на фабрично нулиране или друго умишлено действие.</string>
<string name="meshtastic_new_nodes_notifications">Известия за нови възли</string> <string name="meshtastic_new_nodes_notifications">Известия за нови възли</string>
<string name="snr">SNR</string> <string name="snr">SNR</string>
<string name="snr_definition">Съотношение сигнал/шум, мярка, използвана в комуникациите за количествено определяне на нивото на желания сигнал спрямо нивото на фоновия шум. В Meshtastic и други безжични системи, по-високото съотношение сигнал/шум показва по-ясен сигнал, който може да подобри надеждността и качеството на предаване на данни.</string>
<string name="rssi">RSSI</string> <string name="rssi">RSSI</string>
<string name="rssi_definition">Индикатор за силата на получения сигнал - измерване, използвано за определяне на нивото на получения сигнал, приемано от антената. По-високата стойност на RSSI обикновено показва по-силна и по-стабилна връзка.</string>
<string name="iaq_definition">(Качество на въздуха в помещенията) относителна скала за IAQ, стойностите са измерени с Bosch BME680. Диапазон на стойностите 0500.</string> <string name="iaq_definition">(Качество на въздуха в помещенията) относителна скала за IAQ, стойностите са измерени с Bosch BME680. Диапазон на стойностите 0500.</string>
<string name="device_metrics_log">Метрики на устройството</string> <string name="device_metrics_log">Метрики на устройството</string>
<string name="node_map">Карта на възела</string>
<string name="position_log">Позиция</string> <string name="position_log">Позиция</string>
<string name="last_position_update">Последна актуализация на позицията</string> <string name="last_position_update">Последна актуализация на позицията</string>
<string name="env_metrics_log">Показатели на околната среда</string> <string name="env_metrics_log">Показатели на околната среда</string>
@ -362,7 +366,6 @@
<string name="one_month">1М</string> <string name="one_month">1М</string>
<string name="max">Макс</string> <string name="max">Макс</string>
<string name="min">Мин</string> <string name="min">Мин</string>
<string name="avg">Ср</string>
<string name="expand_chart">Разгъване на диаграмата</string> <string name="expand_chart">Разгъване на диаграмата</string>
<string name="collapse_chart">Свиване на диаграмата</string> <string name="collapse_chart">Свиване на диаграмата</string>
<string name="unknown_age">Неизвестна възраст</string> <string name="unknown_age">Неизвестна възраст</string>
@ -483,6 +486,17 @@
<string name="frequency_slot">Честотен слот</string> <string name="frequency_slot">Честотен слот</string>
<string name="ignore_mqtt">Игнориране на MQTT</string> <string name="ignore_mqtt">Игнориране на MQTT</string>
<string name="mqtt_config">Конфигуриране на 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="mqtt_enabled">MQTT е активиран</string>
<string name="address">Адрес</string> <string name="address">Адрес</string>
<string name="username">Потребителско име</string> <string name="username">Потребителско име</string>
@ -960,4 +974,9 @@
<string name="wifi_provision_ssid_placeholder">Въведете или изберете мрежа</string> <string name="wifi_provision_ssid_placeholder">Въведете или изберете мрежа</string>
<string name="wifi_provision_status_applied">WiFi е конфигуриран успешно!</string> <string name="wifi_provision_status_applied">WiFi е конфигуриран успешно!</string>
<string name="wifi_provision_status_failed">Прилагането на конфигурацията за 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> </resources>

View file

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

View file

@ -331,12 +331,9 @@
<string name="userinfo">Informace o uživateli</string> <string name="userinfo">Informace o uživateli</string>
<string name="meshtastic_new_nodes_notifications">Oznámení o nových uzlech</string> <string name="meshtastic_new_nodes_notifications">Oznámení o nových uzlech</string>
<string name="snr">SNR</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">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="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="device_metrics_log">Metriky zařízení</string>
<string name="node_map">Mapa uzlu</string>
<string name="position_log">Pozice</string> <string name="position_log">Pozice</string>
<string name="last_position_update">Poslední aktualizace pozice</string> <string name="last_position_update">Poslední aktualizace pozice</string>
<string name="env_metrics_log">Metriky prostředí</string> <string name="env_metrics_log">Metriky prostředí</string>
@ -525,6 +522,8 @@
<string name="ignore_mqtt">Ignorovat MQTT</string> <string name="ignore_mqtt">Ignorovat MQTT</string>
<string name="ok_to_mqtt">OK do MQTT</string> <string name="ok_to_mqtt">OK do MQTT</string>
<string name="mqtt_config">Nastavení 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="mqtt_enabled">MQTT povoleno</string>
<string name="address">Adresa</string> <string name="address">Adresa</string>
<string name="username">Uživatelské jméno</string> <string name="username">Uživatelské jméno</string>
@ -968,4 +967,6 @@
<string name="note">Poznámka</string> <string name="note">Poznámka</string>
<string name="connect">Připojit</string> <string name="connect">Připojit</string>
<string name="done">Hotovo</string> <string name="done">Hotovo</string>
<string name="desktop_notification_title">Meshtastic</string>
<string name="filter_icon">Filtr</string>
</resources> </resources>

View file

@ -253,10 +253,13 @@
<string name="reset_to_defaults">Auf Standardeinstellungen zurücksetzen</string> <string name="reset_to_defaults">Auf Standardeinstellungen zurücksetzen</string>
<string name="apply">Anwenden</string> <string name="apply">Anwenden</string>
<string name="theme">Design</string> <string name="theme">Design</string>
<string name="contrast">Kontrast</string>
<string name="theme_light">Hell</string> <string name="theme_light">Hell</string>
<string name="theme_dark">Dunkel</string> <string name="theme_dark">Dunkel</string>
<string name="theme_system">System</string> <string name="theme_system">System</string>
<string name="choose_theme">Design auswählen</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_medium">Medium Fast</string>
<string name="contrast_high">Hoch</string> <string name="contrast_high">Hoch</string>
<string name="provide_location_to_mesh">Standort zum Mesh angeben</string> <string name="provide_location_to_mesh">Standort zum Mesh angeben</string>
@ -356,6 +359,8 @@
<string name="battery">Akku</string> <string name="battery">Akku</string>
<string name="channel_utilization">Kanalauslastung</string> <string name="channel_utilization">Kanalauslastung</string>
<string name="air_utilization">Sendezeit</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_numeric_value">%1$s</string>
<string name="device_metrics_label_value">%1$s: %2$s</string> <string name="device_metrics_label_value">%1$s: %2$s</string>
<string name="temperature">Temperatur</string> <string name="temperature">Temperatur</string>
@ -378,12 +383,9 @@
<string name="userinfo">Benutzerinfo</string> <string name="userinfo">Benutzerinfo</string>
<string name="meshtastic_new_nodes_notifications">Benachrichtigung neue Knoten</string> <string name="meshtastic_new_nodes_notifications">Benachrichtigung neue Knoten</string>
<string name="snr">SNR</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">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="iaq_definition">(Innenluftqualität) relativer IAQ-Wert gemessen von Bosch BME680.</string>
<string name="device_metrics_log">Gerätedaten</string> <string name="device_metrics_log">Gerätedaten</string>
<string name="node_map">Standortkarte Knoten</string>
<string name="position_log">Standort</string> <string name="position_log">Standort</string>
<string name="last_position_update">Letzte Standortaktualisierung</string> <string name="last_position_update">Letzte Standortaktualisierung</string>
<string name="env_metrics_log">Umweltdaten</string> <string name="env_metrics_log">Umweltdaten</string>
@ -430,7 +432,6 @@
<string name="one_month">1 Monat</string> <string name="one_month">1 Monat</string>
<string name="max">Maximal</string> <string name="max">Maximal</string>
<string name="min">Minimum</string> <string name="min">Minimum</string>
<string name="avg">Durchschnitt</string>
<string name="expand_chart">Diagramm einblenden</string> <string name="expand_chart">Diagramm einblenden</string>
<string name="collapse_chart">Diagramm ausblenden</string> <string name="collapse_chart">Diagramm ausblenden</string>
<string name="unknown_age">Alter unbekannt</string> <string name="unknown_age">Alter unbekannt</string>
@ -582,6 +583,9 @@
<string name="output_duration_milliseconds">Ausgabedauer (GPIO)</string> <string name="output_duration_milliseconds">Ausgabedauer (GPIO)</string>
<string name="nag_timeout_seconds">Nervige Verzögerung (Sekunden)</string> <string name="nag_timeout_seconds">Nervige Verzögerung (Sekunden)</string>
<string name="ringtone">Klingelton</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="play">Wiedergabe</string>
<string name="use_i2s_as_buzzer">I2S als Buzzer verwenden</string> <string name="use_i2s_as_buzzer">I2S als Buzzer verwenden</string>
<string name="lora_config">LoRa</string> <string name="lora_config">LoRa</string>
@ -605,6 +609,23 @@
<string name="ignore_mqtt">MQTT ignorieren</string> <string name="ignore_mqtt">MQTT ignorieren</string>
<string name="ok_to_mqtt">OK für MQTT</string> <string name="ok_to_mqtt">OK für MQTT</string>
<string name="mqtt_config">MQTT Einstellungen</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="mqtt_enabled">MQTT aktiviert</string>
<string name="address">Adresse</string> <string name="address">Adresse</string>
<string name="username">Benutzername</string> <string name="username">Benutzername</string>
@ -720,6 +741,7 @@
<string name="rainfall_24h">Regen (24 Std.)</string> <string name="rainfall_24h">Regen (24 Std.)</string>
<string name="weight">Gewicht</string> <string name="weight">Gewicht</string>
<string name="radiation">Strahlung</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="store_forward_config"><![CDATA[Speichern & Weiterleiten Einstellungen]]></string>
<string name="indoor_air_quality_iaq">Luftqualität im Innenbereich (IAQ)</string> <string name="indoor_air_quality_iaq">Luftqualität im Innenbereich (IAQ)</string>
<string name="url">URL</string> <string name="url">URL</string>
@ -1199,4 +1221,21 @@
<string name="wifi_provision_ssid_placeholder">Netzwerk eingeben oder auswählen</string> <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_applied">WLAN erfolgreich konfiguriert!</string>
<string name="wifi_provision_status_failed">WLAN Konfiguration konnte nicht angewendet werden</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> </resources>

View file

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

View file

@ -300,13 +300,10 @@
<string name="encryption_error">Clave pública no coincide</string> <string name="encryption_error">Clave pública no coincide</string>
<string name="meshtastic_new_nodes_notifications">Notificaciones de nuevo nodo</string> <string name="meshtastic_new_nodes_notifications">Notificaciones de nuevo nodo</string>
<string name="snr">SNR</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">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. <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> Rango de Valores 0 - 500.</string>
<string name="device_metrics_log">Métricas de Dispositivo</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="position_log">Posición</string>
<string name="last_position_update">Última actualización</string> <string name="last_position_update">Última actualización</string>
<string name="env_metrics_log">Métricas de Entorno</string> <string name="env_metrics_log">Métricas de Entorno</string>
@ -492,6 +489,8 @@ Rango de Valores 0 - 500.</string>
<string name="ignore_mqtt">Ignorar Paquetes MQTT</string> <string name="ignore_mqtt">Ignorar Paquetes MQTT</string>
<string name="ok_to_mqtt">Permitir MQTT</string> <string name="ok_to_mqtt">Permitir MQTT</string>
<string name="mqtt_config">Configuración 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="mqtt_enabled">Activar el MQTT</string>
<string name="address">Dirección del Servidor MQTT</string> <string name="address">Dirección del Servidor MQTT</string>
<string name="username">Usuario</string> <string name="username">Usuario</string>
@ -836,4 +835,6 @@ Estos datos de ubicación pueden ser utilizados para fines como aparecer en un m
<string name="tak_team_green">Verde</string> <string name="tak_team_green">Verde</string>
<string name="connect">Conectar</string> <string name="connect">Conectar</string>
<string name="done">Hecho</string> <string name="done">Hecho</string>
<string name="desktop_notification_title">Meshtastic</string>
<string name="filter_icon">Filtro</string>
</resources> </resources>

View file

@ -383,12 +383,9 @@
<string name="userinfo">Kasutaja teave</string> <string name="userinfo">Kasutaja teave</string>
<string name="meshtastic_new_nodes_notifications">Uue sõlme teade</string> <string name="meshtastic_new_nodes_notifications">Uue sõlme teade</string>
<string name="snr">SNR</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">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="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="device_metrics_log">Seadme mõõdikud</string>
<string name="node_map">Sõlmede kaart</string>
<string name="position_log">Asukoht</string> <string name="position_log">Asukoht</string>
<string name="last_position_update">Viimase asukoha värskendus</string> <string name="last_position_update">Viimase asukoha värskendus</string>
<string name="env_metrics_log">Keskkonnamõõdikud</string> <string name="env_metrics_log">Keskkonnamõõdikud</string>
@ -435,7 +432,6 @@
<string name="one_month">1k</string> <string name="one_month">1k</string>
<string name="max">Maksimaalselt</string> <string name="max">Maksimaalselt</string>
<string name="min">Min</string> <string name="min">Min</string>
<string name="avg">Keskm</string>
<string name="expand_chart">Laienda diagrammi</string> <string name="expand_chart">Laienda diagrammi</string>
<string name="collapse_chart">Ahenda diagrammi</string> <string name="collapse_chart">Ahenda diagrammi</string>
<string name="unknown_age">Tundmatu vanus</string> <string name="unknown_age">Tundmatu vanus</string>
@ -613,6 +609,23 @@
<string name="ignore_mqtt">Keela MQTT</string> <string name="ignore_mqtt">Keela MQTT</string>
<string name="ok_to_mqtt">Ok MQTTi</string> <string name="ok_to_mqtt">Ok MQTTi</string>
<string name="mqtt_config">MQTT sätted</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="mqtt_enabled">MQTT lubatud</string>
<string name="address">Aadress</string> <string name="address">Aadress</string>
<string name="username">Kasutajatunnus</string> <string name="username">Kasutajatunnus</string>
@ -1208,4 +1221,21 @@
<string name="wifi_provision_ssid_placeholder">Sisestage või valige võrk</string> <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_applied">WiFi edukalt seadistatud!</string>
<string name="wifi_provision_status_failed">WiFi sätete rakendamine ebaõnnestus</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> </resources>

View file

@ -383,12 +383,9 @@
<string name="userinfo">Käyttäjätiedot</string> <string name="userinfo">Käyttäjätiedot</string>
<string name="meshtastic_new_nodes_notifications">Uuden laitteen ilmoitukset</string> <string name="meshtastic_new_nodes_notifications">Uuden laitteen ilmoitukset</string>
<string name="snr">SNR</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">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="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="device_metrics_log">Laitteen mittausloki</string>
<string name="node_map">Laitekartta</string>
<string name="position_log">Sijainti</string> <string name="position_log">Sijainti</string>
<string name="last_position_update">Viimeisin sijainnin päivitys</string> <string name="last_position_update">Viimeisin sijainnin päivitys</string>
<string name="env_metrics_log">Ympäristöarvot</string> <string name="env_metrics_log">Ympäristöarvot</string>
@ -435,7 +432,6 @@
<string name="one_month">1 kk</string> <string name="one_month">1 kk</string>
<string name="max">Kaikki</string> <string name="max">Kaikki</string>
<string name="min">Minimi</string> <string name="min">Minimi</string>
<string name="avg">Keskiarvo</string>
<string name="expand_chart">Laajenna kaavio</string> <string name="expand_chart">Laajenna kaavio</string>
<string name="collapse_chart">Pienennä kaavio</string> <string name="collapse_chart">Pienennä kaavio</string>
<string name="unknown_age">Tuntematon ikä</string> <string name="unknown_age">Tuntematon ikä</string>
@ -613,6 +609,23 @@
<string name="ignore_mqtt">Ohita MQTT</string> <string name="ignore_mqtt">Ohita MQTT</string>
<string name="ok_to_mqtt">MQTT päällä</string> <string name="ok_to_mqtt">MQTT päällä</string>
<string name="mqtt_config">MQTT asetukset</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="mqtt_enabled">MQTT käytössä</string>
<string name="address">Osoite</string> <string name="address">Osoite</string>
<string name="username">Käyttäjänimi</string> <string name="username">Käyttäjänimi</string>
@ -1209,4 +1222,21 @@
<string name="wifi_provision_ssid_placeholder">Syötä tai valitse verkko</string> <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_applied">WiFi määritetty onnistuneesti!</string>
<string name="wifi_provision_status_failed">WiFi-asetusten käyttöönotto epäonnistui</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> </resources>

View file

@ -18,6 +18,7 @@
<resources> <resources>
<string name="meshtastic_app_name">Meshtastic</string> <string name="meshtastic_app_name">Meshtastic</string>
<!-- Language tags native names (not available via .getDisplayLanguage) --> <!-- 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="node_filter_placeholder">Filtre</string>
<string name="desc_node_filter_clear">Effacer le filtre de nœud</string> <string name="desc_node_filter_clear">Effacer le filtre de nœud</string>
<string name="node_filter_title">Filtrer par</string> <string name="node_filter_title">Filtrer par</string>
@ -40,9 +41,11 @@
<string name="internal">Interne</string> <string name="internal">Interne</string>
<string name="node_sort_via_favorite">par Favoris</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_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="unrecognized">Non reconnu</string>
<string name="message_status_enroute">En attente d'accusé de réception</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_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_unknown">Inconnu</string>
<string name="message_status_sfpp_routing">Routage via chaîne SF++…</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> <string name="message_status_sfpp_confirmed">Confirmé via chaîne SF++</string>
@ -119,7 +122,8 @@
<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_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_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_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 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_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_security_private_key">Utilisée pour créer une clé partagée avec un appareil distant.</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_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> <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>
@ -163,18 +167,25 @@
<string name="ip_port">Port :</string> <string name="ip_port">Port :</string>
<string name="connected">Connecté</string> <string name="connected">Connecté</string>
<string name="connection_status">Connexions actuelles :</string> <string name="connection_status">Connexions actuelles :</string>
<string name="wifi_ip">IP WiFi :</string> <string name="wifi_ip">IP du Wifi :</string>
<string name="ethernet_ip">IP Ethernet :</string> <string name="ethernet_ip">IP Ethernet :</string>
<string name="connecting">Connexion en cours</string> <string name="connecting">Connexion en cours</string>
<string name="not_connected">Non connecté</string> <string name="not_connected">Non connecté</string>
<string name="no_device_selected">Aucun appareil sélectionné</string> <string name="no_device_selected">Aucun appareil sélectionné</string>
<string name="unknown_device">Périphérique inconnu</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="connected_sleeping">Connecté à la radio, mais en mode veille</string>
<string name="app_too_old">Mise à jour de lapplication requise</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="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="none">Aucun (désactivé)</string>
<string name="meshtastic_service_notifications">Notifications de service</string> <string name="meshtastic_service_notifications">Notifications de service</string>
<string name="acknowledgements">Remerciements</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="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_panel">Panneau de débogage</string>
<string name="debug_decoded_payload">Contenu décodé :</string> <string name="debug_decoded_payload">Contenu décodé :</string>
@ -207,7 +218,21 @@
<string name="match_all">Correspondre à tout | N'importe quel</string> <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="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="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="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="message_delivery_status">Statut d'envoi du message</string>
<string name="new_messages_below">Nouveaux messages au-dessous</string> <string name="new_messages_below">Nouveaux messages au-dessous</string>
<string name="meshtastic_messages_notifications">Notifications de message</string> <string name="meshtastic_messages_notifications">Notifications de message</string>
@ -228,10 +253,15 @@
<string name="reset_to_defaults">Rétablir les valeurs par défaut</string> <string name="reset_to_defaults">Rétablir les valeurs par défaut</string>
<string name="apply">Appliquer</string> <string name="apply">Appliquer</string>
<string name="theme">Thème</string> <string name="theme">Thème</string>
<string name="contrast">Contraste</string>
<string name="theme_light">Clair</string> <string name="theme_light">Clair</string>
<string name="theme_dark">Sombre</string> <string name="theme_dark">Sombre</string>
<string name="theme_system">Valeur par défaut du système</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_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="provide_location_to_mesh">Fournir l'emplacement au maillage</string>
<string name="use_homoglyph_characters_encoding">Encodage compact pour Cyrillique</string> <string name="use_homoglyph_characters_encoding">Encodage compact pour Cyrillique</string>
<plurals name="delete_messages"> <plurals name="delete_messages">
@ -275,6 +305,7 @@
<string name="direct_message">Message direct</string> <string name="direct_message">Message direct</string>
<string name="nodedb_reset">Reconfiguration de NodeDB</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">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="error">Erreur</string>
<string name="unknown_error">Une erreur inconnue s'est produite</string> <string name="unknown_error">Une erreur inconnue s'est produite</string>
<string name="ignore">Ignorer</string> <string name="ignore">Ignorer</string>
@ -317,6 +348,8 @@
<string name="currently">Actuellement :</string> <string name="currently">Actuellement :</string>
<string name="mute_status_always">Toujours muet</string> <string name="mute_status_always">Toujours muet</string>
<string name="mute_status_unmuted">Non 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_add">Désactiver les notifications pour '%1$s' ?</string>
<string name="mute_remove">Réactiver les notifications pour '%1$s' ?</string> <string name="mute_remove">Réactiver les notifications pour '%1$s' ?</string>
<string name="replace">Remplacer</string> <string name="replace">Remplacer</string>
@ -326,7 +359,10 @@
<string name="battery">Batterie</string> <string name="battery">Batterie</string>
<string name="channel_utilization">UtilCanal</string> <string name="channel_utilization">UtilCanal</string>
<string name="air_utilization">UtilAir</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_numeric_value">%1$s</string>
<string name="device_metrics_label_value">%1$s: %2$s</string>
<string name="temperature">Temp</string> <string name="temperature">Temp</string>
<string name="humidity">Hum</string> <string name="humidity">Hum</string>
<string name="soil_temperature">Temp sol</string> <string name="soil_temperature">Temp sol</string>
@ -347,12 +383,9 @@
<string name="userinfo">Infos utilisateur</string> <string name="userinfo">Infos utilisateur</string>
<string name="meshtastic_new_nodes_notifications">Notifikasyon nouvo nœud</string> <string name="meshtastic_new_nodes_notifications">Notifikasyon nouvo nœud</string>
<string name="snr">SNR</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">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="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="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="position_log">Position</string>
<string name="last_position_update">Dernière mise à jour de position</string> <string name="last_position_update">Dernière mise à jour de position</string>
<string name="env_metrics_log">Métriques d'environnement</string> <string name="env_metrics_log">Métriques d'environnement</string>
@ -381,13 +414,26 @@
<string name="traceroute_duration">Durée : %1$s s</string> <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_towards_dest">Route aller :\n\n</string>
<string name="traceroute_route_back_to_us">Route retour :\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="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="one_hour_short">1H</string>
<string name="twenty_four_hours">24H</string> <string name="twenty_four_hours">24H</string>
<string name="one_week">1S</string> <string name="one_week">1S</string>
<string name="two_weeks">2S</string> <string name="two_weeks">2S</string>
<string name="one_month">1M</string> <string name="one_month">1M</string>
<string name="max">Max</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="unknown_age">Age inconnu</string>
<string name="copy">Copier</string> <string name="copy">Copier</string>
<string name="alert_bell_text">Caractère d'appel !</string> <string name="alert_bell_text">Caractère d'appel !</string>
@ -401,11 +447,17 @@
<string name="channel_1">Canal 1</string> <string name="channel_1">Canal 1</string>
<string name="channel_2">Canal 2</string> <string name="channel_2">Canal 2</string>
<string name="channel_3">Canal 3</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="current">Actif</string>
<string name="voltage">Tension</string> <string name="voltage">Tension</string>
<string name="are_you_sure">Êtes-vous sûr ?</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="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="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="meshtastic_low_battery_notifications">Notifications de batterie faible</string>
<string name="low_battery_title">Batterie faible : %1$s</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> <string name="meshtastic_low_battery_temporary_remote_notifications">Notifications de batterie faible (nœuds favoris)</string>
@ -531,6 +583,9 @@
<string name="output_duration_milliseconds">Durée de sortie (en millisecondes)</string> <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="nag_timeout_seconds">Durée de répétition de la sortie (secondes)</string>
<string name="ringtone">Sonnerie</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="play">Lancer</string>
<string name="use_i2s_as_buzzer">Utiliser l'I2S comme buzzer</string> <string name="use_i2s_as_buzzer">Utiliser l'I2S comme buzzer</string>
<string name="lora_config">LoRa</string> <string name="lora_config">LoRa</string>
@ -554,6 +609,13 @@
<string name="ignore_mqtt">Ignorer MQTT</string> <string name="ignore_mqtt">Ignorer MQTT</string>
<string name="ok_to_mqtt">Transmission des paquets vers MQTT</string> <string name="ok_to_mqtt">Transmission des paquets vers MQTT</string>
<string name="mqtt_config">Configuration 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="mqtt_enabled">MQTT activé</string>
<string name="address">Adresse</string> <string name="address">Adresse</string>
<string name="username">Nom d'utilisateur</string> <string name="username">Nom d'utilisateur</string>
@ -625,6 +687,8 @@
<string name="serial_enabled">Série activée</string> <string name="serial_enabled">Série activée</string>
<string name="echo_enabled">Écho activé</string> <string name="echo_enabled">Écho activé</string>
<string name="serial_baud_rate">Vitesse de transmission série</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="timeout">Délai d'expiration</string>
<string name="serial_mode">Mode série</string> <string name="serial_mode">Mode série</string>
<string name="override_console_serial_port">Outrepasser le port série de la console</string> <string name="override_console_serial_port">Outrepasser le port série de la console</string>
@ -659,8 +723,15 @@
<string name="distance">Distance</string> <string name="distance">Distance</string>
<string name="lux">Lux</string> <string name="lux">Lux</string>
<string name="wind">Vent</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="weight">Poids</string>
<string name="radiation">Radiation</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="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="indoor_air_quality_iaq">Qualité de l'air intérieur (IAQ)</string>
<string name="url">URL</string> <string name="url">URL</string>
@ -677,6 +748,7 @@
<string name="timestamp">Horodatage</string> <string name="timestamp">Horodatage</string>
<string name="heading">En-tête</string> <string name="heading">En-tête</string>
<string name="speed">Vitesse</string> <string name="speed">Vitesse</string>
<string name="speed_kmh">%1$d Km/h</string>
<string name="sats">Sats</string> <string name="sats">Sats</string>
<string name="alt">Alt</string> <string name="alt">Alt</string>
<string name="freq">Fréq</string> <string name="freq">Fréq</string>
@ -742,6 +814,11 @@
<string name="show_waypoints">Afficher les points de repère</string> <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="show_precision_circle">Afficher les cercles de précision</string>
<string name="client_notification">Notification client</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="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_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> <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>
@ -794,7 +871,14 @@
<string name="type_a_message">Composer un message</string> <string name="type_a_message">Composer un message</string>
<string name="pax_metrics_log">Métriques de PAX</string> <string name="pax_metrics_log">Métriques de PAX</string>
<string name="pax">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="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="ble_devices">Appareils Bluetooth</string>
<string name="connected_device">Périphérique connecté</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> <string name="routing_error_rate_limit_exceeded">Limite de débit dépassée. Veuillez réessayer plus tard.</string>
@ -849,6 +933,8 @@
<string name="map_type_terrain">Terrain</string> <string name="map_type_terrain">Terrain</string>
<string name="map_type_hybrid">Hybride</string> <string name="map_type_hybrid">Hybride</string>
<string name="manage_map_layers">Gérer les calques de la carte</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="hide_layer">Ajouter un calque</string>
<string name="show_layer">Afficher le calque</string> <string name="show_layer">Afficher le calque</string>
<string name="remove_layer">Supprimer le calque</string> <string name="remove_layer">Supprimer le calque</string>
@ -856,6 +942,10 @@
<string name="nodes_at_this_location">Nœuds à cet emplacement</string> <string name="nodes_at_this_location">Nœuds à cet emplacement</string>
<string name="selected_map_type">Type de carte sélectionné</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="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="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="provider_name_exists">Le nom du fournisseur existe déjà.</string>
<string name="url_cannot_be_empty">URL ne peut être vide.</string> <string name="url_cannot_be_empty">URL ne peut être vide.</string>
@ -949,6 +1039,7 @@
<string name="firmware_update_release_notes">Notes de Version</string> <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_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_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_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_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> <string name="firmware_update_hash_rejected">Intégrité (hash) du firmware rejetée. Veuillez réessayer ou mettre à jours l'appareil via USB.</string>
@ -1025,8 +1116,10 @@
<string name="bluetooth_feature_config">Configuration</string> <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="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="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_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_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_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_relays">Relais : %1$d (annulé: %2$d)</string>
<string name="local_stats_diagnostics_prefix">Diagnostiques : %1$s</string> <string name="local_stats_diagnostics_prefix">Diagnostiques : %1$s</string>
@ -1040,10 +1133,94 @@
<string name="refresh">Actualiser</string> <string name="refresh">Actualiser</string>
<string name="updated">Mis à jour</string> <string name="updated">Mis à jour</string>
<!-- Network Map Layers --> <!-- 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_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_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_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_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="connect">Connecter</string>
<string name="done">Terminé</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> </resources>

View file

@ -190,10 +190,7 @@
<string name="encryption_pkc">Cóid Poiblí Eochair</string> <string name="encryption_pkc">Cóid Poiblí Eochair</string>
<string name="encryption_error">Mícomhoiriúnacht na heochrach phoiblí</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="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="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="administration">Rialachas</string>
<string name="remote_admin">Rialú iargúlta</string> <string name="remote_admin">Rialú iargúlta</string>
<string name="bad">Go dona</string> <string name="bad">Go dona</string>
@ -213,6 +210,7 @@
</plurals> </plurals>
<string name="traceroute_diff">Céimeanna i dtreo %1$d Céimeanna ar ais %2$d</string> <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="region_frequency_plan">Réigiún</string>
<string name="mqtt_status_disconnected">Na ceangailte</string>
<string name="timeout">Am tráth</string> <string name="timeout">Am tráth</string>
<string name="distance">Sáth</string> <string name="distance">Sáth</string>
<!-- INSECURE_NO_PRECISE State (Yellow Open Lock) --> <!-- INSECURE_NO_PRECISE State (Yellow Open Lock) -->
@ -229,4 +227,5 @@
<!-- Compass --> <!-- Compass -->
<!-- Message Filter --> <!-- Message Filter -->
<!-- Network Map Layers --> <!-- Network Map Layers -->
<string name="filter_icon">Scagaire</string>
</resources> </resources>

View file

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

View file

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

View file

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

View file

@ -186,10 +186,7 @@
<string name="encryption_pkc">Chifreman Kle Piblik</string> <string name="encryption_pkc">Chifreman Kle Piblik</string>
<string name="encryption_error">Pa matche 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="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="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="administration">Administrasyon</string>
<string name="remote_admin">Administrasyon Remote</string> <string name="remote_admin">Administrasyon Remote</string>
<string name="bad">Move</string> <string name="bad">Move</string>
@ -201,6 +198,7 @@
<string name="traceroute_direct">Direk</string> <string name="traceroute_direct">Direk</string>
<string name="traceroute_diff">Hops vèsus %1$d Hops tounen %2$d</string> <string name="traceroute_diff">Hops vèsus %1$d Hops tounen %2$d</string>
<string name="region_frequency_plan">Rejyon</string> <string name="region_frequency_plan">Rejyon</string>
<string name="mqtt_status_disconnected">Dekonekte</string>
<string name="timeout">Tan pase</string> <string name="timeout">Tan pase</string>
<string name="distance">Distans</string> <string name="distance">Distans</string>
<!-- INSECURE_NO_PRECISE State (Yellow Open Lock) --> <!-- INSECURE_NO_PRECISE State (Yellow Open Lock) -->
@ -217,4 +215,5 @@
<!-- Compass --> <!-- Compass -->
<!-- Message Filter --> <!-- Message Filter -->
<!-- Network Map Layers --> <!-- Network Map Layers -->
<string name="filter_icon">Filtre</string>
</resources> </resources>

View file

@ -316,12 +316,9 @@
<string name="encryption_error">Publikus kulcs nem egyezik</string> <string name="encryption_error">Publikus kulcs nem egyezik</string>
<string name="meshtastic_new_nodes_notifications">Új állomás értesítések</string> <string name="meshtastic_new_nodes_notifications">Új állomás értesítések</string>
<string name="snr">SNR</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">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="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="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="position_log">Pozíció</string>
<string name="last_position_update">Utolsó pozíciófrissítés</string> <string name="last_position_update">Utolsó pozíciófrissítés</string>
<string name="env_metrics_log">Környezeti metrikák</string> <string name="env_metrics_log">Környezeti metrikák</string>
@ -515,6 +512,8 @@
<string name="ignore_mqtt">MQTT figyelmen kívül hagyása</string> <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="ok_to_mqtt">MQTT-re továbbítható</string>
<string name="mqtt_config">MQTT beállítások</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="mqtt_enabled">MQTT engedélyezve</string>
<string name="address">Cím</string> <string name="address">Cím</string>
<string name="username">Felhasználónév</string> <string name="username">Felhasználónév</string>
@ -850,4 +849,6 @@
<string name="tak_team_blue">Kék</string> <string name="tak_team_blue">Kék</string>
<string name="tak_team_green">Zöld</string> <string name="tak_team_green">Zöld</string>
<string name="connect">Csatlakozás</string> <string name="connect">Csatlakozás</string>
<string name="desktop_notification_title">Meshtastic</string>
<string name="filter_icon">Filter</string>
</resources> </resources>

View file

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

View file

@ -355,12 +355,9 @@
<string name="userinfo">Informazioni Utente</string> <string name="userinfo">Informazioni Utente</string>
<string name="meshtastic_new_nodes_notifications">Notifiche di nuovi nodi</string> <string name="meshtastic_new_nodes_notifications">Notifiche di nuovi nodi</string>
<string name="snr">SNR</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">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="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="device_metrics_log">Metriche Dispositivo</string>
<string name="node_map">Mappa Dei Nodi</string>
<string name="position_log">Posizione</string> <string name="position_log">Posizione</string>
<string name="last_position_update">Aggiornamento ultima posizione</string> <string name="last_position_update">Aggiornamento ultima posizione</string>
<string name="env_metrics_log">Metriche Ambientali</string> <string name="env_metrics_log">Metriche Ambientali</string>
@ -560,6 +557,8 @@
<string name="ignore_mqtt">Ignora MQTT</string> <string name="ignore_mqtt">Ignora MQTT</string>
<string name="ok_to_mqtt">OK per MQTT</string> <string name="ok_to_mqtt">OK per MQTT</string>
<string name="mqtt_config">Configurazione 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="mqtt_enabled">MQTT abilitato</string>
<string name="address">Indirizzo</string> <string name="address">Indirizzo</string>
<string name="username">Username</string> <string name="username">Username</string>
@ -960,4 +959,6 @@
<string name="note">Note</string> <string name="note">Note</string>
<string name="connect">Connetti</string> <string name="connect">Connetti</string>
<string name="done">Fatto</string> <string name="done">Fatto</string>
<string name="desktop_notification_title">Meshtastic</string>
<string name="filter_icon">Filtro</string>
</resources> </resources>

View file

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

View file

@ -213,11 +213,8 @@
<string name="encryption_error">공개 키가 일치하지 않습니다</string> <string name="encryption_error">공개 키가 일치하지 않습니다</string>
<string name="meshtastic_new_nodes_notifications">새로운 노드 알림</string> <string name="meshtastic_new_nodes_notifications">새로운 노드 알림</string>
<string name="snr">SNR</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">RSSI</string>
<string name="rssi_definition">수신 신호 강도 지표 Received Signal Strength Indicator, RSSI는 안테나가 수신하는 신호의 전력 수준을 측정하는 데 사용되는 지표입니다. RSSI 값이 높을수록 일반적으로 더 강력하고 안정적인 연결을 나타냅니다.</string>
<string name="iaq_definition">(실내공기질) Bosch BME680으로 측정한 상대적 척도 IAQ 값. 범위 0500.</string> <string name="iaq_definition">(실내공기질) Bosch BME680으로 측정한 상대적 척도 IAQ 값. 범위 0500.</string>
<string name="node_map">노드 지도</string>
<string name="position_log">위치</string> <string name="position_log">위치</string>
<string name="last_position_update">최근 위치 업데이트</string> <string name="last_position_update">최근 위치 업데이트</string>
<string name="administration">관리</string> <string name="administration">관리</string>
@ -373,6 +370,8 @@
<string name="pa_fan_disabled">PA fan 비활성화됨</string> <string name="pa_fan_disabled">PA fan 비활성화됨</string>
<string name="ignore_mqtt">MQTT로 부터 수신 무시</string> <string name="ignore_mqtt">MQTT로 부터 수신 무시</string>
<string name="mqtt_config">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="mqtt_enabled">MQTT 활성화</string>
<string name="address">서버 주소</string> <string name="address">서버 주소</string>
<string name="username">사용자명</string> <string name="username">사용자명</string>
@ -539,4 +538,6 @@
<string name="tak_team_blue">파랑</string> <string name="tak_team_blue">파랑</string>
<string name="tak_team_green">초록</string> <string name="tak_team_green">초록</string>
<string name="connect">연결</string> <string name="connect">연결</string>
<string name="desktop_notification_title">Meshtastic</string>
<string name="filter_icon">필터</string>
</resources> </resources>

View file

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

View file

@ -201,11 +201,8 @@
<string name="encryption_error">Publieke sleutel komt niet overeen</string> <string name="encryption_error">Publieke sleutel komt niet overeen</string>
<string name="meshtastic_new_nodes_notifications">Nieuwe node meldingen</string> <string name="meshtastic_new_nodes_notifications">Nieuwe node meldingen</string>
<string name="snr">SNR</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">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="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="position_log">Positie</string>
<string name="administration">Beheer</string> <string name="administration">Beheer</string>
<string name="remote_admin">Extern beheer</string> <string name="remote_admin">Extern beheer</string>
@ -312,6 +309,8 @@
<string name="ignore_incoming">Inkomende negeren</string> <string name="ignore_incoming">Inkomende negeren</string>
<string name="ignore_mqtt">Negeer MQTT</string> <string name="ignore_mqtt">Negeer MQTT</string>
<string name="mqtt_config">MQTT Configuratie</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="mqtt_enabled">MQTT ingeschakeld</string>
<string name="address">Adres</string> <string name="address">Adres</string>
<string name="username">Gebruikersnaam</string> <string name="username">Gebruikersnaam</string>
@ -416,4 +415,5 @@
<string name="tak_team_blue">Blauw</string> <string name="tak_team_blue">Blauw</string>
<string name="tak_team_green">Groen</string> <string name="tak_team_green">Groen</string>
<string name="connect">Verbinding maken</string> <string name="connect">Verbinding maken</string>
<string name="filter_icon">Filter</string>
</resources> </resources>

View file

@ -194,11 +194,8 @@
<string name="encryption_error">Direktemeldinger bruker den nye offentlige nøkkelinfrastrukturen for kryptering. Krever firmware versjon 2.5 eller høyere.</string> <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="meshtastic_new_nodes_notifications">Varsel om nye noder</string>
<string name="snr">SNR</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">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="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="administration">Administrasjon</string>
<string name="remote_admin">Fjernadministrasjon</string> <string name="remote_admin">Fjernadministrasjon</string>
<string name="bad">Dårlig</string> <string name="bad">Dårlig</string>
@ -222,6 +219,7 @@
<string name="copy">Kopier</string> <string name="copy">Kopier</string>
<string name="alert_bell_text">Varsel, bjellekarakter!</string> <string name="alert_bell_text">Varsel, bjellekarakter!</string>
<string name="region_frequency_plan">Region</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="public_key">Offentlig nøkkel</string>
<string name="private_key">Privat nøkkel</string> <string name="private_key">Privat nøkkel</string>
<string name="timeout">Tidsavbrudd</string> <string name="timeout">Tidsavbrudd</string>
@ -240,4 +238,5 @@
<!-- Compass --> <!-- Compass -->
<!-- Message Filter --> <!-- Message Filter -->
<!-- Network Map Layers --> <!-- Network Map Layers -->
<string name="filter_icon">Filter</string>
</resources> </resources>

View file

@ -333,12 +333,9 @@
<string name="userinfo">Informacje o użytkowniku</string> <string name="userinfo">Informacje o użytkowniku</string>
<string name="meshtastic_new_nodes_notifications">Powiadomienia o nowych węzłach</string> <string name="meshtastic_new_nodes_notifications">Powiadomienia o nowych węzłach</string>
<string name="snr">SNR:</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">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="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="device_metrics_log">Metryka urządzenia</string>
<string name="node_map">Ślad na mapie</string>
<string name="position_log">Pozycjonowanie</string> <string name="position_log">Pozycjonowanie</string>
<string name="last_position_update">Ostatnia aktualizacja lokalizacji</string> <string name="last_position_update">Ostatnia aktualizacja lokalizacji</string>
<string name="env_metrics_log">Metryki środowiskowe</string> <string name="env_metrics_log">Metryki środowiskowe</string>
@ -499,6 +496,8 @@
<string name="ignore_mqtt">Zignoruj MQTT</string> <string name="ignore_mqtt">Zignoruj MQTT</string>
<string name="ok_to_mqtt">Ok dla MQTT</string> <string name="ok_to_mqtt">Ok dla MQTT</string>
<string name="mqtt_config">Konfiguracja 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="mqtt_enabled">Włącz MQTT</string>
<string name="address">Adres</string> <string name="address">Adres</string>
<string name="username">Nazwa użytkownika</string> <string name="username">Nazwa użytkownika</string>
@ -749,4 +748,6 @@
<string name="traffic_management_enabled">Moduł Włączony</string> <string name="traffic_management_enabled">Moduł Włączony</string>
<string name="connect">Połącz</string> <string name="connect">Połącz</string>
<string name="done">Wykonano</string> <string name="done">Wykonano</string>
<string name="desktop_notification_title">Meshtastic</string>
<string name="filter_icon">Filtr</string>
</resources> </resources>

View file

@ -230,11 +230,8 @@
<string name="encryption_error">Chave pública não confere</string> <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="meshtastic_new_nodes_notifications">Novas notificações de nó</string>
<string name="snr">SNR</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">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="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="position_log">Posição</string>
<string name="last_position_update">Atualização da última posição</string> <string name="last_position_update">Atualização da última posição</string>
<string name="administration">Administração</string> <string name="administration">Administração</string>
@ -382,6 +379,8 @@
<string name="pa_fan_disabled">Ventilador do PA desativado</string> <string name="pa_fan_disabled">Ventilador do PA desativado</string>
<string name="ignore_mqtt">Ignorar MQTT</string> <string name="ignore_mqtt">Ignorar MQTT</string>
<string name="mqtt_config">Configurações 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="mqtt_enabled">MQTT habilitado</string>
<string name="address">Endereço</string> <string name="address">Endereço</string>
<string name="username">Nome de usuário</string> <string name="username">Nome de usuário</string>
@ -665,4 +664,6 @@
<string name="tak_team_blue">Azul</string> <string name="tak_team_blue">Azul</string>
<string name="tak_team_green">Verde</string> <string name="tak_team_green">Verde</string>
<string name="done">Concluído</string> <string name="done">Concluído</string>
<string name="desktop_notification_title">Meshtastic</string>
<string name="filter_icon">Filtro</string>
</resources> </resources>

View file

@ -219,11 +219,8 @@
<string name="encryption_error">Incompatibilidade de chave pública</string> <string name="encryption_error">Incompatibilidade de chave pública</string>
<string name="meshtastic_new_nodes_notifications">Notificações de novos nodes</string> <string name="meshtastic_new_nodes_notifications">Notificações de novos nodes</string>
<string name="snr">SNR</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">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="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="position_log">Posição</string>
<string name="administration">Administração</string> <string name="administration">Administração</string>
<string name="remote_admin">Administração Remota</string> <string name="remote_admin">Administração Remota</string>
@ -366,6 +363,8 @@
<string name="ignore_incoming">Ignorar entrada</string> <string name="ignore_incoming">Ignorar entrada</string>
<string name="ignore_mqtt">Ignorar MQTT</string> <string name="ignore_mqtt">Ignorar MQTT</string>
<string name="mqtt_config">Configuração 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="mqtt_enabled">MQTT ativo</string>
<string name="address">Endereço</string> <string name="address">Endereço</string>
<string name="username">Utilizador</string> <string name="username">Utilizador</string>
@ -515,4 +514,6 @@
<string name="tak_team_blue">Azul</string> <string name="tak_team_blue">Azul</string>
<string name="tak_team_green">Verde</string> <string name="tak_team_green">Verde</string>
<string name="connect">Ligar</string> <string name="connect">Ligar</string>
<string name="desktop_notification_title">Nome do nó de alternativo</string>
<string name="filter_icon">Filtrar</string>
</resources> </resources>

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