Compare commits

..

27 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
124 changed files with 2739 additions and 551 deletions

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": [

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

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

@ -40,15 +40,6 @@
-dontwarn org.bouncycastle.** -dontwarn org.bouncycastle.**
-dontwarn org.openjsse.** -dontwarn org.openjsse.**
# ---- Compose Runtime & Animation -------------------------------------------- # Compose runtime/ui/animation/foundation/material3 keep rules now live in
# config/proguard/shared-rules.pro so both Android (R8) and desktop (ProGuard)
# Defence-in-depth: prevent R8 tree-shaking of Compose infrastructure classes # get the same defence-in-depth coverage against CMP 1.11 optimizer folding.
# that are referenced indirectly through compiler-generated state machines.
# With -dontoptimize above these are largely redundant, but they provide a
# safety net against future toolchain changes.
-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

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

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

@ -20,12 +20,10 @@
-keepattributes SourceFile,LineNumberTable,*Annotation*,Signature,InnerClasses,EnclosingMethod,Exceptions,RuntimeVisibleAnnotations -keepattributes SourceFile,LineNumberTable,*Annotation*,Signature,InnerClasses,EnclosingMethod,Exceptions,RuntimeVisibleAnnotations
# ---- Kotlin / Coroutines ---------------------------------------------------- # ---- Kotlin / Coroutines ----------------------------------------------------
# Kotlin stdlib and kotlinx-coroutines ship their own consumer ProGuard rules
-keep class kotlin.Metadata { *; } # (kotlin-stdlib and kotlinx-coroutines-core consumer-rules.pro) which keep
-keep class kotlin.reflect.** { *; } # Metadata, Continuation, kotlin.reflect internals, and debug metadata. No
-keep class kotlin.coroutines.Continuation { *; } # explicit wildcards needed here.
-keep class kotlinx.coroutines.** { *; }
-dontwarn kotlinx.coroutines.**
# ---- Koin DI (reflection-based injection) ----------------------------------- # ---- Koin DI (reflection-based injection) -----------------------------------
@ -41,9 +39,7 @@
-keep @org.koin.core.annotation.ComponentScan class * { *; } -keep @org.koin.core.annotation.ComponentScan class * { *; }
-keep @org.koin.core.annotation.Single class * { *; } -keep @org.koin.core.annotation.Single class * { *; }
-keep @org.koin.core.annotation.Factory class * { *; } -keep @org.koin.core.annotation.Factory class * { *; }
-keep @org.koin.core.annotation.KoinViewModel class * { *; }
# Generated Koin module extensions (Koin Annotations plugin output)
-keep class org.meshtastic.**.di.** { *; }
# ---- kotlinx-serialization -------------------------------------------------- # ---- kotlinx-serialization --------------------------------------------------
@ -63,13 +59,14 @@
# ---- Wire Protobuf ---------------------------------------------------------- # ---- Wire Protobuf ----------------------------------------------------------
# Wire generates ADAPTER companion objects accessed via reflection # Wire generates an ADAPTER static field on every Message subclass accessed
-keep class com.squareup.wire.** { *; } # reflectively during encoding/decoding. Keep those fields and the
-dontwarn com.squareup.wire.** # ProtoAdapter subclasses themselves; Wire's bundled consumer rules preserve
# the runtime itself.
# Generated proto message classes (both meshtastic protos and internal package) -keepclassmembers class * extends com.squareup.wire.Message {
-keep class org.meshtastic.proto.** { *; } public static *** ADAPTER;
-keep class meshtastic.** { *; } }
-keepclassmembers class * extends com.squareup.wire.ProtoAdapter { *; }
# Suppress warnings about missing Android Parcelable (Wire cross-platform stubs # Suppress warnings about missing Android Parcelable (Wire cross-platform stubs
# when compiling for non-Android JVM targets; harmless on Android). # when compiling for non-Android JVM targets; harmless on Android).
@ -86,40 +83,24 @@
-keep class org.meshtastic.core.database.MeshtasticDatabaseConstructor { *; } -keep class org.meshtastic.core.database.MeshtasticDatabaseConstructor { *; }
-keep class org.meshtastic.core.database.MeshtasticDatabase { *; } -keep class org.meshtastic.core.database.MeshtasticDatabase { *; }
# Room DAOs Room generates implementations at compile time; keep interfaces # Room's own consumer rules (from androidx.room3) keep DAOs, entities,
-keep class org.meshtastic.core.database.dao.** { *; } # generated _Impl classes, and TypeConverters referenced from the database.
# Room Entities accessed via reflection for column mapping
-keep class org.meshtastic.core.database.entity.** { *; }
# Room TypeConverters invoked reflectively
-keep class org.meshtastic.core.database.Converters { *; }
# Room generated _Impl classes
-keep class **_Impl { *; }
# ---- SQLite bundled -------------------------------------------------------- # ---- SQLite bundled --------------------------------------------------------
# androidx.sqlite ships consumer rules.
-keep class androidx.sqlite.** { *; }
-dontwarn androidx.sqlite.**
# ---- Ktor (ServiceLoader + plugin discovery) -------------------------------- # ---- Ktor (ServiceLoader + plugin discovery) --------------------------------
-keep class io.ktor.** { *; } # Keep ServiceLoader metadata files (ktor discovers HttpClientEngineFactory
-dontwarn io.ktor.** # implementations reflectively via ServiceLoader).
# Keep ServiceLoader metadata files
-keepclassmembers class * implements io.ktor.client.HttpClientEngineFactory { *; } -keepclassmembers class * implements io.ktor.client.HttpClientEngineFactory { *; }
# ---- Coil 3 (image loading) ------------------------------------------------- # ---- Coil 3 (image loading) -------------------------------------------------
# coil3 ships consumer rules.
-keep class coil3.** { *; }
-dontwarn coil3.**
# ---- Kable BLE -------------------------------------------------------------- # ---- Kable BLE --------------------------------------------------------------
# com.juul.kable ships consumer rules; if release builds fail with missing
-keep class com.juul.kable.** { *; } # Kable classes, restore a narrow keep for the specific reflection-loaded type.
-dontwarn com.juul.kable.**
# ---- Compose Multiplatform resources ---------------------------------------- # ---- Compose Multiplatform resources ----------------------------------------
@ -127,17 +108,14 @@
# Without these the fdroid flavor has crashed at startup with a misleading # Without these the fdroid flavor has crashed at startup with a misleading
# URLDecodeException due to R8 exception-class merging. # URLDecodeException due to R8 exception-class merging.
-keep class org.jetbrains.compose.resources.** { *; } -keep class org.jetbrains.compose.resources.** { *; }
-keep class org.meshtastic.core.resources.** { *; } -keep class org.meshtastic.core.resources.Res { *; }
-keepclassmembers class org.meshtastic.core.resources.Res$* { *; }
# ---- AboutLibraries --------------------------------------------------------- # ---- AboutLibraries ---------------------------------------------------------
# com.mikepenz.aboutlibraries ships consumer rules.
-keep class com.mikepenz.aboutlibraries.** { *; }
-dontwarn com.mikepenz.aboutlibraries.**
# ---- Multiplatform Markdown Renderer ---------------------------------------- # ---- Multiplatform Markdown Renderer ----------------------------------------
# com.mikepenz.markdown ships consumer rules.
-keep class com.mikepenz.markdown.** { *; }
-dontwarn com.mikepenz.markdown.**
# ---- QR Code Kotlin --------------------------------------------------------- # ---- QR Code Kotlin ---------------------------------------------------------
@ -147,33 +125,42 @@
-dontwarn qrcode.** -dontwarn qrcode.**
# ---- Kermit logging --------------------------------------------------------- # ---- Kermit logging ---------------------------------------------------------
# co.touchlab.kermit ships consumer rules.
-keep class co.touchlab.kermit.** { *; }
-dontwarn co.touchlab.kermit.**
# ---- Okio ------------------------------------------------------------------- # ---- Okio -------------------------------------------------------------------
# okio ships consumer rules.
-keep class okio.** { *; }
-dontwarn okio.**
# ---- DataStore -------------------------------------------------------------- # ---- DataStore --------------------------------------------------------------
# androidx.datastore ships consumer rules.
-keep class androidx.datastore.** { *; }
-dontwarn androidx.datastore.**
# ---- Paging ----------------------------------------------------------------- # ---- Paging -----------------------------------------------------------------
# androidx.paging ships consumer rules.
-keep class androidx.paging.** { *; }
-dontwarn androidx.paging.**
# ---- Lifecycle / Navigation 3 / ViewModel (JetBrains forks) ----------------- # ---- Lifecycle / Navigation 3 / ViewModel (JetBrains forks) -----------------
# androidx.lifecycle and androidx.navigation3 ship consumer rules.
-keep class androidx.lifecycle.** { *; }
-keep class androidx.navigation3.** { *; }
-dontwarn androidx.lifecycle.**
-dontwarn androidx.navigation3.**
# ---- Meshtastic shared model ------------------------------------------------ # ---- 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.
# Core model classes (used in serialization, Room, and Koin injection) # ---- Compose Runtime & Animation --------------------------------------------
-keep class org.meshtastic.core.model.** { *; }
# 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

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

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

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

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

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

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

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

@ -37,18 +37,20 @@ abstract class StreamTransport(protected val callback: RadioTransportCallback, p
override suspend 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

@ -78,7 +78,11 @@ open class TcpRadioTransport(
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)
} }
} }
} }
@ -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

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

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

@ -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>
@ -220,4 +222,5 @@
<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="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,5 +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="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>
@ -200,4 +201,5 @@
<!-- Message Filter --> <!-- Message Filter -->
<!-- Network Map Layers --> <!-- Network Map Layers -->
<string name="desktop_notification_title">Meshtastic</string> <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>
@ -969,4 +968,5 @@
<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="desktop_notification_title">Meshtastic</string>
<string name="filter_icon">Filtr</string>
</resources> </resources>

View file

@ -383,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>
@ -435,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>
@ -613,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>
@ -1208,5 +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="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>
@ -837,4 +836,5 @@ Estos datos de ubicación pueden ser utilizados para fines como aparecer en un m
<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="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,5 +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="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,5 +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="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,11 +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="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) -->
@ -168,4 +169,5 @@
<!-- 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="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>
@ -851,4 +850,5 @@
<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="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>
@ -961,4 +960,5 @@
<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="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>
@ -653,4 +652,5 @@
<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="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>
@ -540,4 +539,5 @@
<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="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>
@ -750,4 +749,5 @@
<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="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>
@ -666,4 +665,5 @@
<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="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>
@ -516,4 +515,5 @@
<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="desktop_notification_title">Nome do nó de alternativo</string>
<string name="filter_icon">Filtrar</string>
</resources> </resources>

View file

@ -376,12 +376,9 @@
<string name="userinfo">Info utilizator</string> <string name="userinfo">Info utilizator</string>
<string name="meshtastic_new_nodes_notifications">Notificări noduri noi</string> <string name="meshtastic_new_nodes_notifications">Notificări noduri noi</string>
<string name="snr">SNR</string> <string name="snr">SNR</string>
<string name="snr_definition">Raportul semnal-zgomot (Signal-to-Noise Ratio), o măsură utilizată în comunicații pentru a cuantifica nivelul unui semnal dorit în raport cu nivelul zgomotului de fond. În Meshtastic și în alte sisteme wireless, un SNR mai mare indică un semnal mai clar, care poate îmbunătăți fiabilitatea și calitatea transmiterii datelor.</string>
<string name="rssi"> RSSI</string> <string name="rssi"> RSSI</string>
<string name="rssi_definition">Indicatorul intensității semnalului recepționat (Received Signal Strength Indicator), o măsurătoare utilizată pentru a determina nivelul de putere recepționat de antenă. O valoare RSSI mai mare indică, în general, o conexiune mai puternică și mai stabilă.</string>
<string name="iaq_definition">(Calitatea aerului interior) valoarea IAQ pe o scară relativă, măsurată cu Bosch BME680. Intervalul valorilor: 0500.</string> <string name="iaq_definition">(Calitatea aerului interior) valoarea IAQ pe o scară relativă, măsurată cu Bosch BME680. Intervalul valorilor: 0500.</string>
<string name="device_metrics_log">Valori dispozitiv</string> <string name="device_metrics_log">Valori dispozitiv</string>
<string name="node_map">Harta nodurilor</string>
<string name="position_log">Poziție</string> <string name="position_log">Poziție</string>
<string name="last_position_update">Ultima actualizare a poziției</string> <string name="last_position_update">Ultima actualizare a poziției</string>
<string name="env_metrics_log">Indicatori de mediu</string> <string name="env_metrics_log">Indicatori de mediu</string>
@ -424,7 +421,6 @@
<string name="one_week">1W</string> <string name="one_week">1W</string>
<string name="two_weeks">2W</string> <string name="two_weeks">2W</string>
<string name="max">Maxim</string> <string name="max">Maxim</string>
<string name="avg">Medie</string>
<string name="expand_chart">Extindeți graficul</string> <string name="expand_chart">Extindeți graficul</string>
<string name="collapse_chart">Restrânge graficul</string> <string name="collapse_chart">Restrânge graficul</string>
<string name="unknown_age">Vârstă necunoscută</string> <string name="unknown_age">Vârstă necunoscută</string>
@ -599,6 +595,8 @@
<string name="ignore_mqtt">Ignoră MQTT</string> <string name="ignore_mqtt">Ignoră MQTT</string>
<string name="ok_to_mqtt">Acceptă MQTT</string> <string name="ok_to_mqtt">Acceptă MQTT</string>
<string name="mqtt_config">Configurare MQTT</string> <string name="mqtt_config">Configurare MQTT</string>
<string name="mqtt_status_disconnected">Deconectat</string>
<string name="mqtt_status_connected">Conectat</string>
<string name="mqtt_enabled">MQTT activat</string> <string name="mqtt_enabled">MQTT activat</string>
<string name="address">Adresă</string> <string name="address">Adresă</string>
<string name="username">Nume de utilizator</string> <string name="username">Nume de utilizator</string>
@ -1169,4 +1167,5 @@
<string name="wifi_provision_status_applied">WiFi configurat cu succes!</string> <string name="wifi_provision_status_applied">WiFi configurat cu succes!</string>
<string name="wifi_provision_status_failed">Nu s-a reușit aplicarea configurației Wi-Fi</string> <string name="wifi_provision_status_failed">Nu s-a reușit aplicarea configurației Wi-Fi</string>
<string name="desktop_notification_title">Meshtastic</string> <string name="desktop_notification_title">Meshtastic</string>
<string name="filter_icon">Filtru</string>
</resources> </resources>

View file

@ -389,12 +389,9 @@
<string name="userinfo">Пользовательская информация</string> <string name="userinfo">Пользовательская информация</string>
<string name="meshtastic_new_nodes_notifications">Уведомления о новых нодах</string> <string name="meshtastic_new_nodes_notifications">Уведомления о новых нодах</string>
<string name="snr">Сигнал/шум</string> <string name="snr">Сигнал/шум</string>
<string name="snr_definition">Соотношение сигнал/шум, мера, используемая в коммуникациях для количественной оценки уровня желаемого сигнала по отношению к уровню фонового шума. В Meshtastic и других беспроводных системах более высокий SNR указывает на более четкий сигнал, который может повысить надежность и качество передачи данных.</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>
@ -443,7 +440,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>
@ -621,6 +617,23 @@
<string name="ignore_mqtt">Игнорировать MQTT</string> <string name="ignore_mqtt">Игнорировать MQTT</string>
<string name="ok_to_mqtt">ОК в MQTT</string> <string name="ok_to_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_disconnected_with_reason">Отключено — %1$s</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_running">Проверяем брокер…</string>
<string name="mqtt_probe_success">Доступно. Брокер принял учетные данные.</string>
<string name="mqtt_probe_success_with_info">Доступно (%1$s)</string>
<string name="mqtt_probe_rejected">Брокер отклонен: %1$s</string>
<string name="mqtt_probe_dns_failure">Узел не найден</string>
<string name="mqtt_probe_tcp_failure">Не удается подключиться к брокеру (TCP)</string>
<string name="mqtt_probe_tls_failure">Сбой TLS-рукопожатия</string>
<string name="mqtt_probe_timeout">Тайм-аут после %1$d мс</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>
@ -1224,5 +1237,21 @@
<string name="wifi_provision_ssid_placeholder">Введите или выберите сеть</string> <string name="wifi_provision_ssid_placeholder">Введите или выберите сеть</string>
<string name="wifi_provision_status_applied">Wi-Fi успешно настроен!</string> <string name="wifi_provision_status_applied">Wi-Fi успешно настроен!</string>
<string name="wifi_provision_status_failed">Не удалось применить настройку Wi-Fi</string> <string name="wifi_provision_status_failed">Не удалось применить настройку Wi-Fi</string>
<string name="desktop_tray_tooltip">Meshtastic Desktop</string>
<string name="desktop_tray_show">Показать Meshtastic</string>
<string name="desktop_tray_quit">Выход</string>
<string name="desktop_notification_title">Meshtastic</string> <string name="desktop_notification_title">Meshtastic</string>
<string name="export_tak_data_package">Экспорт пакета данных TAK</string>
<string name="clear_time_zone">Очистить часовой пояс</string>
<string name="filter_icon">Фильтр</string>
<string name="remove_filter">Удалить фильтр</string>
<string name="show_iaq_legend">Показать легенду качества воздуха</string>
<string name="action_show_message_status">Показать статус сообщения</string>
<string name="action_send_reply">Отправить ответ</string>
<string name="action_copy_message">Скопировать сообщение</string>
<string name="action_select_message">Выбрать сообщение</string>
<string name="action_delete_message">Удалить сообщение</string>
<string name="action_react_with_emoji">Отреагировать эмодзи</string>
<string name="action_select_device">Выберите устройство</string>
<string name="action_select_network">Выбрать сеть</string>
</resources> </resources>

View file

@ -257,11 +257,8 @@
<string name="encryption_error">Nezhoda verejného kľúča</string> <string name="encryption_error">Nezhoda verejného kľúča</string>
<string name="meshtastic_new_nodes_notifications">Notifikácie nových uzlov</string> <string name="meshtastic_new_nodes_notifications">Notifikácie nových uzlov</string>
<string name="snr">SNR</string> <string name="snr">SNR</string>
<string name="snr_definition">Pomer signálu od šumu (SNR), miera používaná v komunikácii na kvantifikáciu úrovne požadovaného signálu k úrovni hluku pozadia. V Meshtastic a iných bezdrôtových systémoch znamená vyšší SNR jasnejší signál, ktorý môže zvýšiť spoľahlivosť a kvalitu prenosu údajov.</string>
<string name="rssi">RSSI</string> <string name="rssi">RSSI</string>
<string name="rssi_definition">Indikátor sily prijímaného signálu (RSSI), meranie používané na určenie úrovne výkonu prijatého skrz anténu. Vyššia hodnota RSSI vo všeobecnosti znamená silnejšie a stabilnejšie pripojenie.</string>
<string name="iaq_definition">(Kvalita vzduchu v interiéri) relatívna hodnota IAQ meraná prístrojom Bosch BME680. Rozsah hodnôt 0500.</string> <string name="iaq_definition">(Kvalita vzduchu v interiéri) relatívna hodnota IAQ meraná prístrojom Bosch BME680. Rozsah hodnôt 0500.</string>
<string name="node_map">Mapa uzlov</string>
<string name="position_log">Pozícia</string> <string name="position_log">Pozícia</string>
<string name="administration">Administrácia</string> <string name="administration">Administrácia</string>
<string name="remote_admin">Administrácia na diaľku</string> <string name="remote_admin">Administrácia na diaľku</string>
@ -362,6 +359,8 @@
<string name="lora_config">LoRa</string> <string name="lora_config">LoRa</string>
<string name="bandwidth">Šírka pásma</string> <string name="bandwidth">Šírka pásma</string>
<string name="region_frequency_plan">Región</string> <string name="region_frequency_plan">Región</string>
<string name="mqtt_status_disconnected">Odpojené</string>
<string name="mqtt_status_connected">Pripojený</string>
<string name="address">Adresa</string> <string name="address">Adresa</string>
<string name="username">Používateľské meno</string> <string name="username">Používateľské meno</string>
<string name="password">Heslo</string> <string name="password">Heslo</string>
@ -428,4 +427,5 @@
<string name="tak_team_blue">Modrá</string> <string name="tak_team_blue">Modrá</string>
<string name="tak_team_green">Zelená</string> <string name="tak_team_green">Zelená</string>
<string name="desktop_notification_title">Meshtastic</string> <string name="desktop_notification_title">Meshtastic</string>
<string name="filter_icon">Filter</string>
</resources> </resources>

View file

@ -196,11 +196,8 @@
<string name="encryption_error">Neujemanje javnega ključa</string> <string name="encryption_error">Neujemanje javnega ključa</string>
<string name="meshtastic_new_nodes_notifications">Obvestila novih vozlišč</string> <string name="meshtastic_new_nodes_notifications">Obvestila novih vozlišč</string>
<string name="snr">SNR</string> <string name="snr">SNR</string>
<string name="snr_definition">Razmerje med signalom in šumom je merilo, ki se uporablja v komunikacijah za količinsko opredelitev ravni želenega signala glede na raven hrupa v ozadju. V Meshtastic in drugih brezžičnih sistemih višji SNR pomeni jasnejši signal, ki lahko poveča zanesljivost in kakovost prenosa podatkov.</string>
<string name="rssi">RSSI</string> <string name="rssi">RSSI</string>
<string name="rssi_definition">Indikator moči sprejetega signala je meritev, ki se uporablja za določanje ravni moči, ki jo sprejema antena. Višja vrednost RSSI na splošno pomeni močnejšo in stabilnejšo povezavo.</string>
<string name="iaq_definition">(Kakovost zraka v zaprtih prostorih) relativna vrednost IAQ na lestvici, izmerjena z Bosch BME680. Razpon vrednosti 0500.</string> <string name="iaq_definition">(Kakovost zraka v zaprtih prostorih) relativna vrednost IAQ na lestvici, izmerjena z Bosch BME680. Razpon vrednosti 0500.</string>
<string name="node_map">Zemljevid vozlišč</string>
<string name="administration">Administracija</string> <string name="administration">Administracija</string>
<string name="remote_admin">Administracija na daljavo</string> <string name="remote_admin">Administracija na daljavo</string>
<string name="bad">Slab</string> <string name="bad">Slab</string>
@ -226,6 +223,7 @@
<string name="copy">Kopiraj</string> <string name="copy">Kopiraj</string>
<string name="alert_bell_text">Znak opozorilnega zvonca!</string> <string name="alert_bell_text">Znak opozorilnega zvonca!</string>
<string name="region_frequency_plan">Regija</string> <string name="region_frequency_plan">Regija</string>
<string name="mqtt_status_disconnected">Prekinjeno</string>
<string name="public_key">Javni ključ</string> <string name="public_key">Javni ključ</string>
<string name="private_key">Zasebni ključ</string> <string name="private_key">Zasebni ključ</string>
<string name="timeout">Časovna omejitev</string> <string name="timeout">Časovna omejitev</string>
@ -244,4 +242,5 @@
<!-- Compass --> <!-- Compass -->
<!-- Message Filter --> <!-- Message Filter -->
<!-- Network Map Layers --> <!-- Network Map Layers -->
<string name="filter_icon">Filter</string>
</resources> </resources>

View file

@ -186,10 +186,7 @@
<string name="encryption_pkc">Kriptimi me Çelës Publik</string> <string name="encryption_pkc">Kriptimi me Çelës Publik</string>
<string name="encryption_error">Përputhje e Gabuar e Çelësit Publik</string> <string name="encryption_error">Përputhje e Gabuar e Çelësit Publik</string>
<string name="meshtastic_new_nodes_notifications">Njoftimet për nyje të reja</string> <string name="meshtastic_new_nodes_notifications">Njoftimet për nyje të reja</string>
<string name="snr_definition">Raporti i Sinjalit në Zhurmë, një masë e përdorur në komunikime për të kuantifikuar nivelin e një sinjali të dëshiruar ndaj nivelit të zhurmës në background. Në Meshtastic dhe sisteme të tjera pa tel, një SNR më i lartë tregon një sinjal më të pastër që mund të rrisë besueshmërinë dhe cilësinë e transmetimit të të dhënave.</string>
<string name="rssi_definition">Indikatori i Fuqisë së Sinjalit të Marrë, një matje e përdorur për të përcaktuar nivelin e energjisë që po merret nga antena. Një vlerë më e lartë RSSI zakonisht tregon një lidhje më të fortë dhe më të qëndrueshme.</string>
<string name="iaq_definition">(Cilësia e Ajrit të Brendshëm) shkalla relative e vlerës IAQ siç matet nga Bosch BME680. Intervali i Vlerave 0500.</string> <string name="iaq_definition">(Cilësia e Ajrit të Brendshëm) shkalla relative e vlerës IAQ siç matet nga Bosch BME680. Intervali i Vlerave 0500.</string>
<string name="node_map">Harta e Nyjës</string>
<string name="administration">Administratë</string> <string name="administration">Administratë</string>
<string name="remote_admin">Administratë e Largët</string> <string name="remote_admin">Administratë e Largët</string>
<string name="bad">I Keq</string> <string name="bad">I Keq</string>
@ -202,6 +199,7 @@
<string name="traceroute_diff">Hops drejt %1$d Hops prapa %2$d</string> <string name="traceroute_diff">Hops drejt %1$d Hops prapa %2$d</string>
<string name="messages">訊息</string> <string name="messages">訊息</string>
<string name="region_frequency_plan">Rajon</string> <string name="region_frequency_plan">Rajon</string>
<string name="mqtt_status_disconnected">I shkëputur</string>
<string name="timeout">Koha e skaduar</string> <string name="timeout">Koha e skaduar</string>
<string name="distance">Distanca</string> <string name="distance">Distanca</string>
<!-- INSECURE_NO_PRECISE State (Yellow Open Lock) --> <!-- INSECURE_NO_PRECISE State (Yellow Open Lock) -->
@ -218,4 +216,5 @@
<!-- Compass --> <!-- Compass -->
<!-- Message Filter --> <!-- Message Filter -->
<!-- Network Map Layers --> <!-- Network Map Layers -->
<string name="filter_icon">Filtrimi</string>
</resources> </resources>

View file

@ -241,12 +241,9 @@
<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">Однос сигнал/шум SNR је мера која се користи у комуникацијама за квантитативно одређивање нивоа жељеног сигнала у односу на ниво позадинског шума. У Мештастик и другим бежичним системима, већи SNR указује на јаснији сигнал који може побољшати поузданост и квалитет преноса података.</string>
<string name="rssi">RSSI</string> <string name="rssi">RSSI</string>
<string name="rssi_definition">Indikator jačine primljenog signala RSSI, merenje koje se koristi za određivanje nivoa snage koji antena prima. Viša vrednost RSSI generalno ukazuje na jaču i stabilniju vezu.</string>
<string name="iaq_definition">(Kvalitet vazduha u zatvorenom prostoru) relativna skala vrednosti IAQ merena Bosch BME680. Raspon vrednosti 0500.</string> <string name="iaq_definition">(Kvalitet vazduha u zatvorenom prostoru) relativna skala vrednosti IAQ merena Bosch BME680. Raspon vrednosti 0500.</string>
<string name="device_metrics_log">Метрика уређаја</string> <string name="device_metrics_log">Метрика уређаја</string>
<string name="node_map">Mapa čvorova</string>
<string name="position_log">Позиција</string> <string name="position_log">Позиција</string>
<string name="env_metrics_log">Метрике сензора</string> <string name="env_metrics_log">Метрике сензора</string>
<string name="administration">Administracija</string> <string name="administration">Administracija</string>
@ -349,6 +346,8 @@
<string name="ignore_mqtt">Игнориши MQTT</string> <string name="ignore_mqtt">Игнориши MQTT</string>
<string name="ok_to_mqtt">Позитиван за MQTT</string> <string name="ok_to_mqtt">Позитиван за MQTT</string>
<string name="mqtt_config">MQTT подешавања</string> <string name="mqtt_config">MQTT подешавања</string>
<string name="mqtt_status_disconnected">Raskačeno</string>
<string name="mqtt_status_connected">Блутут повезан</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>
@ -430,4 +429,5 @@
<string name="bluetooth_permission">Блутут</string> <string name="bluetooth_permission">Блутут</string>
<string name="powered">Напајано</string> <string name="powered">Напајано</string>
<!-- Network Map Layers --> <!-- Network Map Layers -->
<string name="filter_icon">Filter</string>
</resources> </resources>

View file

@ -241,12 +241,9 @@
<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">Однос сигнал/шум SNR је мера која се користи у комуникацијама за квантитативно одређивање нивоа жељеног сигнала у односу на ниво позадинског шума. У Мештастик и другим бежичним системима, већи SNR указује на јаснији сигнал који може побољшати поузданост и квалитет преноса података.</string>
<string name="rssi">RSSI</string> <string name="rssi">RSSI</string>
<string name="rssi_definition">Индикатор јачине примљеног сигнала RSSI је мера која се користи за одређивање нивоа снаге која се прима преко антене. Виша вредност RSSI генерално указује на јачу и стабилнију везу.</string>
<string name="iaq_definition">Индекс квалитета ваздуха (IAQ) као мера за одређивање квалитета ваздуха унутрашњости, мерен са Bosch BME680. Вредности се крећу у распону од 0 до 500.</string> <string name="iaq_definition">Индекс квалитета ваздуха (IAQ) као мера за одређивање квалитета ваздуха унутрашњости, мерен са Bosch BME680. Вредности се крећу у распону од 0 до 500.</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="env_metrics_log">Метрике сензора</string> <string name="env_metrics_log">Метрике сензора</string>
<string name="administration">Администрација</string> <string name="administration">Администрација</string>
@ -349,6 +346,8 @@
<string name="ignore_mqtt">Игнориши MQTT</string> <string name="ignore_mqtt">Игнориши MQTT</string>
<string name="ok_to_mqtt">Позитиван за MQTT</string> <string name="ok_to_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="address">Адреса</string> <string name="address">Адреса</string>
<string name="username">Корисничко име</string> <string name="username">Корисничко име</string>
<string name="password">Лозинка</string> <string name="password">Лозинка</string>
@ -430,4 +429,5 @@
<string name="bluetooth_permission">Блутут</string> <string name="bluetooth_permission">Блутут</string>
<string name="powered">Напајано</string> <string name="powered">Напајано</string>
<!-- Network Map Layers --> <!-- Network Map Layers -->
<string name="filter_icon">Филтер</string>
</resources> </resources>

View file

@ -43,6 +43,7 @@
<string name="unrecognized">Okänd</string> <string name="unrecognized">Okänd</string>
<string name="message_status_enroute">Inväntar kvittens</string> <string name="message_status_enroute">Inväntar kvittens</string>
<string name="message_status_queued">Kvittens köad</string> <string name="message_status_queued">Kvittens köad</string>
<string name="message_status_delivered">Levererad till nät</string>
<string name="message_status_unknown">Okänd</string> <string name="message_status_unknown">Okänd</string>
<string name="routing_error_none">Kvitterad</string> <string name="routing_error_none">Kvitterad</string>
<string name="routing_error_no_route">Ingen rutt</string> <string name="routing_error_no_route">Ingen rutt</string>
@ -339,12 +340,9 @@
<string name="userinfo">Användarinfo</string> <string name="userinfo">Användarinfo</string>
<string name="meshtastic_new_nodes_notifications">Ny nod avisering</string> <string name="meshtastic_new_nodes_notifications">Ny nod avisering</string>
<string name="snr">SNR</string> <string name="snr">SNR</string>
<string name="snr_definition">Signal-to-Noise Ratio, är ett mått som används inom kommunikation för att kvantifiera nivån av en önskad signal mot nivån av bakgrundsbrus. I Meshtastic och andra trådlösa system indikerar en högre SNR en tydligare signal som kan förbättra tillförlitligheten och kvaliteten på dataöverföringen.</string>
<string name="rssi">RSSI</string> <string name="rssi">RSSI</string>
<string name="rssi_definition">Received Signal Strength Indicator, ett mått som används för att avgöra effektnivån som togs emot av antennen. Ett högre RSSI-värde indikerar generellt en starkare och stabilare anslutning.</string>
<string name="iaq_definition">(Indoor Air Quality) relativ skala IAQ värdet mätt med Bosch BME600. Värdeintervall 0-500.</string> <string name="iaq_definition">(Indoor Air Quality) relativ skala IAQ värdet mätt med Bosch BME600. Värdeintervall 0-500.</string>
<string name="device_metrics_log">Enhetens mätvärden</string> <string name="device_metrics_log">Enhetens mätvärden</string>
<string name="node_map">Nod karta</string>
<string name="position_log">Plats</string> <string name="position_log">Plats</string>
<string name="last_position_update">Senaste positionsuppdatering</string> <string name="last_position_update">Senaste positionsuppdatering</string>
<string name="env_metrics_log">Miljövärden</string> <string name="env_metrics_log">Miljövärden</string>
@ -373,6 +371,7 @@
<string name="traceroute_duration">Varaktighet: %1$s s</string> <string name="traceroute_duration">Varaktighet: %1$s s</string>
<string name="traceroute_route_towards_dest">Rutt spårad mot destination:\n\n</string> <string name="traceroute_route_towards_dest">Rutt spårad mot destination:\n\n</string>
<string name="traceroute_route_back_to_us">Rutten spårad tillbaka till oss:\n\n</string> <string name="traceroute_route_back_to_us">Rutten spårad tillbaka till oss:\n\n</string>
<string name="traceroute_no_response">Inget svar</string>
<string name="one_hour_short">1h</string> <string name="one_hour_short">1h</string>
<string name="twenty_four_hours">24T</string> <string name="twenty_four_hours">24T</string>
<string name="one_week">1V</string> <string name="one_week">1V</string>
@ -529,6 +528,10 @@
<string name="ignore_mqtt">Ignorera MQTT</string> <string name="ignore_mqtt">Ignorera MQTT</string>
<string name="ok_to_mqtt">Ok till MQTT</string> <string name="ok_to_mqtt">Ok till MQTT</string>
<string name="mqtt_config">MQTT-konfiguration</string> <string name="mqtt_config">MQTT-konfiguration</string>
<string name="mqtt_status_disconnected">Frånkopplad</string>
<string name="mqtt_status_connected">Ansluten</string>
<string name="mqtt_test_connection">Testa anslutningen</string>
<string name="mqtt_probe_other_failure">Anslutningen misslyckades</string>
<string name="mqtt_enabled">MQTT är aktiverat</string> <string name="mqtt_enabled">MQTT är aktiverat</string>
<string name="address">Adress</string> <string name="address">Adress</string>
<string name="username">Användarnamn</string> <string name="username">Användarnamn</string>
@ -946,4 +949,6 @@
<string name="connect">Anslut</string> <string name="connect">Anslut</string>
<string name="done">Klart</string> <string name="done">Klart</string>
<string name="desktop_notification_title">Meshtastic</string> <string name="desktop_notification_title">Meshtastic</string>
<string name="filter_icon">Filter</string>
<string name="action_select_device">Välj enhet</string>
</resources> </resources>

View file

@ -213,11 +213,8 @@
<string name="encryption_error">Genel Anahtar Uyuşmazlığı</string> <string name="encryption_error">Genel Anahtar Uyuşmazlığı</string>
<string name="meshtastic_new_nodes_notifications">Yeni düğüm bildirimleri</string> <string name="meshtastic_new_nodes_notifications">Yeni düğüm bildirimleri</string>
<string name="snr">SNR</string> <string name="snr">SNR</string>
<string name="snr_definition">Sinyal-Gürültü Oranı, iletişimde istenen bir sinyalin seviyesini arka plan gürültüsü seviyesine mukayese ölçmek için kullanılan bir ölçüdür. Meshtastic ve diğer kablosuz sistemlerde, daha yüksek bir SNR, veri iletiminin güvenilirliğini ve kalitesini artırabilecek daha net bir sinyale işaret eder.</string>
<string name="rssi">RSSI</string> <string name="rssi">RSSI</string>
<string name="rssi_definition">Alınan Sinyal Gücü Göstergesi, anten tarafından alınan güç seviyesini belirlemek için kullanılan bir ölçüdür. Daha yüksek bir RSSI değeri genellikle daha güçlü ve daha istikrarlı bir bağlantıya işaret eder.</string>
<string name="iaq_definition">(İç Hava Kalitesi) Bosch BME680 tarafından ölçülen bağıl ölçekli IAQ değeri. Değer Aralığı 0500.</string> <string name="iaq_definition">(İç Hava Kalitesi) Bosch BME680 tarafından ölçülen bağıl ölçekli IAQ değeri. Değer Aralığı 0500.</string>
<string name="node_map">Düğüm Haritası</string>
<string name="position_log">Konum</string> <string name="position_log">Konum</string>
<string name="administration">Yönetim</string> <string name="administration">Yönetim</string>
<string name="remote_admin">Uzaktan Yönetim</string> <string name="remote_admin">Uzaktan Yönetim</string>
@ -366,6 +363,8 @@
<string name="pa_fan_disabled">PA fanı devre dışı</string> <string name="pa_fan_disabled">PA fanı devre dışı</string>
<string name="ignore_mqtt">MQTT'yi Yoksay</string> <string name="ignore_mqtt">MQTT'yi Yoksay</string>
<string name="mqtt_config">MQTT Yapılandırması</string> <string name="mqtt_config">MQTT Yapılandırması</string>
<string name="mqtt_status_disconnected">Bağlantı kesildi</string>
<string name="mqtt_status_connected">Bağlandı</string>
<string name="mqtt_enabled">MQTT etkin</string> <string name="mqtt_enabled">MQTT etkin</string>
<string name="address">Adres</string> <string name="address">Adres</string>
<string name="username">Kullanıcı adı</string> <string name="username">Kullanıcı adı</string>
@ -547,4 +546,5 @@
<string name="tak_team_green">Yeşil</string> <string name="tak_team_green">Yeşil</string>
<string name="connect">Bağlan</string> <string name="connect">Bağlan</string>
<string name="desktop_notification_title">Meshtastic</string> <string name="desktop_notification_title">Meshtastic</string>
<string name="filter_icon">Filtre</string>
</resources> </resources>

View file

@ -277,9 +277,7 @@
<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="rssi">RSSI</string> <string name="rssi">RSSI</string>
<string name="rssi_definition">Показник рівня потужності сигналу — вимірювання, що використовується для визначення рівня потужності, що приймається антеною. Вище значення RSSI зазвичай вказує на міцніше та стабільніше з'єднання.</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="env_metrics_log">Показники довкілля</string> <string name="env_metrics_log">Показники довкілля</string>
<string name="administration">Адміністрування</string> <string name="administration">Адміністрування</string>
@ -401,6 +399,9 @@
<string name="override_frequency_mhz">Перевизначити частоту</string> <string name="override_frequency_mhz">Перевизначити частоту</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_test_connection">Перевірка зʼєднання</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>
@ -722,4 +723,6 @@
<string name="connect">Під’єднатися</string> <string name="connect">Під’єднатися</string>
<string name="done">Готово</string> <string name="done">Готово</string>
<string name="desktop_notification_title">Meshtastic</string> <string name="desktop_notification_title">Meshtastic</string>
<string name="filter_icon">Фільтри</string>
<string name="action_select_device">Оберіть пристрій</string>
</resources> </resources>

View file

@ -362,12 +362,9 @@
<string name="userinfo">用户信息</string> <string name="userinfo">用户信息</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 及其他无线系统中,较高的信噪比表示信号更加清晰,从而能够提升数据传输的可靠性和质量。</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">室内空气质量Indoor Air Quality, IAQ由 Bosch BME680 传感器测量的相对标尺 IAQ 值,取值范围为 0500。</string> <string name="iaq_definition">室内空气质量Indoor Air Quality, IAQ由 Bosch BME680 传感器测量的相对标尺 IAQ 值,取值范围为 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>
@ -568,6 +565,9 @@
<string name="ignore_mqtt">忽略 MQTT</string> <string name="ignore_mqtt">忽略 MQTT</string>
<string name="ok_to_mqtt">使用MQTT</string> <string name="ok_to_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_test_connection">连接测试</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>
@ -1115,4 +1115,6 @@
<string name="connect">连接</string> <string name="connect">连接</string>
<string name="done">完成</string> <string name="done">完成</string>
<string name="desktop_notification_title">Meshtastic</string> <string name="desktop_notification_title">Meshtastic</string>
<string name="filter_icon">搜索节点</string>
<string name="action_select_device">选择设备</string>
</resources> </resources>

View file

@ -45,6 +45,7 @@
<string name="unrecognized">無法識別</string> <string name="unrecognized">無法識別</string>
<string name="message_status_enroute">正在等待確認</string> <string name="message_status_enroute">正在等待確認</string>
<string name="message_status_queued">發送佇列中</string> <string name="message_status_queued">發送佇列中</string>
<string name="message_status_delivered">已傳送至 Mesh</string>
<string name="message_status_unknown">不明</string> <string name="message_status_unknown">不明</string>
<string name="message_status_sfpp_routing">透過 SF++ 鏈路由…</string> <string name="message_status_sfpp_routing">透過 SF++ 鏈路由…</string>
<string name="message_status_sfpp_confirmed">已在 SF++ 鏈上確認</string> <string name="message_status_sfpp_confirmed">已在 SF++ 鏈上確認</string>
@ -122,6 +123,7 @@
<string name="config_position_gps_update_interval_summary">嘗試獲取 GPS 位置的頻率(&lt; 10 秒將保持 GPS 模組開啟)。</string> <string name="config_position_gps_update_interval_summary">嘗試獲取 GPS 位置的頻率(&lt; 10 秒將保持 GPS 模組開啟)。</string>
<string name="config_position_flags_summary">位置訊息可選的附加欄位。包含的欄位越多,訊息越大,將造成空中時間拉長和封包遺失的風險增加。</string> <string name="config_position_flags_summary">位置訊息可選的附加欄位。包含的欄位越多,訊息越大,將造成空中時間拉長和封包遺失的風險增加。</string>
<string name="config_power_is_power_saving_summary">將盡可能使所有元件進入睡眠狀態。對於 tracker 和 sensor 角色,此模式將包含 LoRa 無線電。如果您想搭配手機應用程式使用設備,或正在使用沒有使用者按鈕的設備,請勿啟用此設定。</string> <string name="config_power_is_power_saving_summary">將盡可能使所有元件進入睡眠狀態。對於 tracker 和 sensor 角色,此模式將包含 LoRa 無線電。如果您想搭配手機應用程式使用設備,或正在使用沒有使用者按鈕的設備,請勿啟用此設定。</string>
<string name="config_security_public_key">從您的私鑰生成並傳送給網狀網路中的其他節點,以供它們計算出共享密鑰。</string>
<string name="config_security_private_key">用於與遠端設備交換密鑰。</string> <string name="config_security_private_key">用於與遠端設備交換密鑰。</string>
<string name="config_security_admin_key">被授權可對此節點發送管理訊息的公鑰。</string> <string name="config_security_admin_key">被授權可對此節點發送管理訊息的公鑰。</string>
<string name="config_security_is_managed">設備處於受管理狀態,使用者無法變更任何設備設定。</string> <string name="config_security_is_managed">設備處於受管理狀態,使用者無法變更任何設備設定。</string>
@ -220,6 +222,11 @@
<string name="a11y_label_value">%1$s: %2$s</string> <string name="a11y_label_value">%1$s: %2$s</string>
<string name="a11y_message_from">來自 %1$s 的訊息:%2$s</string> <string name="a11y_message_from">來自 %1$s 的訊息:%2$s</string>
<string name="preview_header">標頭</string> <string name="preview_header">標頭</string>
<string name="preview_footer">標尾</string>
<string name="preview_dot">點形</string>
<string name="preview_text">文字</string>
<string name="preview_gauge">儀表板</string>
<string name="preview_gradient">梯度</string>
<string name="preview_custom_composable_line_one">這是一個一個一個可客製化的組合元件</string> <string name="preview_custom_composable_line_one">這是一個一個一個可客製化的組合元件</string>
<string name="preview_custom_composable_line_two">還支援多行文字與多種樣式</string> <string name="preview_custom_composable_line_two">還支援多行文字與多種樣式</string>
<string name="message_delivery_status">訊息傳遞狀態</string> <string name="message_delivery_status">訊息傳遞狀態</string>
@ -371,12 +378,9 @@
<string name="userinfo">使用者資訊</string> <string name="userinfo">使用者資訊</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">信噪比SNR用於通訊中量化所需信號與背景噪音水平的指標。在 Meshtastic 及其他無線系統中,信噪比越高表示信號越清晰,可以提高數據傳輸的可靠性和品質。</string>
<string name="rssi">RSSI</string> <string name="rssi">RSSI</string>
<string name="rssi_definition">接收信號強度指示RSSI用於測量天線所接收到信號的功率強度。 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>
@ -408,6 +412,12 @@
<string name="traceroute_return_hops">回程跳數</string> <string name="traceroute_return_hops">回程跳數</string>
<string name="traceroute_round_trip">來回跳數</string> <string name="traceroute_round_trip">來回跳數</string>
<string name="traceroute_no_response">無回應</string> <string name="traceroute_no_response">無回應</string>
<string name="load_1_min">1分鐘負載</string>
<string name="load_5_min">5分鐘負載</string>
<string name="load_15_min">15分鐘負載</string>
<string name="load_1_min_description">1分鐘系統負載平均值</string>
<string name="load_5_min_description">5分鐘系統負載平均值</string>
<string name="load_15_min_description">15分鐘系統負載平均值</string>
<string name="free_memory_description">可用系統記憶體(位元組)</string> <string name="free_memory_description">可用系統記憶體(位元組)</string>
<string name="one_hour_short">1小時</string> <string name="one_hour_short">1小時</string>
<string name="twenty_four_hours">二十四小時</string> <string name="twenty_four_hours">二十四小時</string>
@ -416,7 +426,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>
@ -594,6 +603,23 @@
<string name="ignore_mqtt">無視MQTT</string> <string name="ignore_mqtt">無視MQTT</string>
<string name="ok_to_mqtt">允許轉發至 MQTT</string> <string name="ok_to_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_disconnected_with_reason">已斷線 — %1$s</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_running">正在查詢 Broker…</string>
<string name="mqtt_probe_success">可供連線Broker 已驗證並接受憑證。</string>
<string name="mqtt_probe_success_with_info">可供連線(%1$s</string>
<string name="mqtt_probe_rejected">Broker 遭拒:%1$s</string>
<string name="mqtt_probe_dns_failure">找不到伺服器</string>
<string name="mqtt_probe_tcp_failure">無法連線至 Broker 中繼伺服器TCP</string>
<string name="mqtt_probe_tls_failure">TLS 握手失敗</string>
<string name="mqtt_probe_timeout">經過 %1$d 毫秒後逾時</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>
@ -792,6 +818,9 @@
<string name="show_waypoints">顯示路徑</string> <string name="show_waypoints">顯示路徑</string>
<string name="show_precision_circle">顯示定位精準度</string> <string name="show_precision_circle">顯示定位精準度</string>
<string name="client_notification">客户端通知</string> <string name="client_notification">客户端通知</string>
<string name="key_verification_title">金鑰驗證</string>
<string name="key_verification_request_title">金鑰驗證請求</string>
<string name="key_verification_final_title">金鑰驗證已完成</string>
<string name="duplicated_public_key_title">偵測到重複的公鑰</string> <string name="duplicated_public_key_title">偵測到重複的公鑰</string>
<string name="low_entropy_key_title">偵測到加密金鑰強度不足</string> <string name="low_entropy_key_title">偵測到加密金鑰強度不足</string>
<string name="compromised_keys">偵測到金鑰已洩漏,點選確定後重新產生金鑰。</string> <string name="compromised_keys">偵測到金鑰已洩漏,點選確定後重新產生金鑰。</string>
@ -853,6 +882,7 @@
<string name="pax_ble_marker">BLE: %1$s</string> <string name="pax_ble_marker">BLE: %1$s</string>
<string name="pax_wifi_marker">WiFi: %1$s</string> <string name="pax_wifi_marker">WiFi: %1$s</string>
<string name="no_pax_metrics_logs">無可用的 PAX 人流計量資料。</string> <string name="no_pax_metrics_logs">無可用的 PAX 人流計量資料。</string>
<string name="wifi_devices">mPWRD-OS 的 Wi-Fi 設定</string>
<string name="ble_devices">藍牙裝置</string> <string name="ble_devices">藍牙裝置</string>
<string name="connected_device">連接裝置</string> <string name="connected_device">連接裝置</string>
<string name="routing_error_rate_limit_exceeded">超過速率限制,請稍後再嘗試。</string> <string name="routing_error_rate_limit_exceeded">超過速率限制,請稍後再嘗試。</string>
@ -1158,6 +1188,9 @@
<string name="traffic_management_router_preserve_hops">保留路由跳數</string> <string name="traffic_management_router_preserve_hops">保留路由跳數</string>
<string name="note">注意</string> <string name="note">注意</string>
<string name="device_storage_ui_title">裝置儲存空間與使用者介面(唯讀)</string> <string name="device_storage_ui_title">裝置儲存空間與使用者介面(唯讀)</string>
<string name="device_theme_language">主題 %1$s語言 %2$s</string>
<string name="files_available">可使用檔案(%1$d</string>
<string name="file_entry">- %1$s%2$d 位元)</string>
<string name="no_files_manifested">未發現任何檔案。</string> <string name="no_files_manifested">未發現任何檔案。</string>
<string name="connect">連線</string> <string name="connect">連線</string>
<string name="done">完成</string> <string name="done">完成</string>
@ -1166,10 +1199,34 @@
<string name="wifi_provision_mpwrd_disclaimer">進一步了解 mPWRD-OS 專案\nhttps://github.com/mPWRD-OS</string> <string name="wifi_provision_mpwrd_disclaimer">進一步了解 mPWRD-OS 專案\nhttps://github.com/mPWRD-OS</string>
<string name="wifi_provision_scanning_ble">正在搜尋裝置…</string> <string name="wifi_provision_scanning_ble">正在搜尋裝置…</string>
<string name="wifi_provision_device_found">找到裝置</string> <string name="wifi_provision_device_found">找到裝置</string>
<string name="wifi_provision_device_found_detail">準備好掃描 Wi-Fi 網路了。</string>
<string name="wifi_provision_scan_networks">搜尋網路</string>
<string name="wifi_provision_scanning_wifi">正在搜尋…</string> <string name="wifi_provision_scanning_wifi">正在搜尋…</string>
<string name="wifi_provision_sending_credentials">正在套用 Wi-Fi 設定…</string>
<string name="wifi_provision_no_networks">找不到網路</string> <string name="wifi_provision_no_networks">找不到網路</string>
<string name="wifi_provision_connect_failed">無法連接:%1$s</string>
<string name="wifi_provision_scan_failed">無法搜尋到 Wi-Fi 網路:%1$s</string>
<string name="wifi_provision_signal_strength">%1$d%</string> <string name="wifi_provision_signal_strength">%1$d%</string>
<string name="wifi_provision_available_networks">可用的網路</string> <string name="wifi_provision_available_networks">可用的網路</string>
<string name="wifi_provision_ssid_label">網路名稱SSID</string> <string name="wifi_provision_ssid_label">網路名稱SSID</string>
<string name="wifi_provision_ssid_placeholder">手動輸入或選擇一個網路</string>
<string name="wifi_provision_status_applied">Wi-Fi 已設定完成!</string>
<string name="wifi_provision_status_failed">無法套用 Wi-Fi 設定</string>
<string name="desktop_tray_tooltip">Meshtastic Desktop</string>
<string name="desktop_tray_show">顯示 Meshtastic</string>
<string name="desktop_tray_quit">離開</string>
<string name="desktop_notification_title">Meshtastic</string> <string name="desktop_notification_title">Meshtastic</string>
<string name="export_tak_data_package">匯出 TAK 資料封包</string>
<string name="clear_time_zone">清除時區</string>
<string name="filter_icon">過濾器</string>
<string name="remove_filter">移除篩選條件</string>
<string name="show_iaq_legend">顯示空氣品質圖例</string>
<string name="action_show_message_status">顯示訊息狀態</string>
<string name="action_send_reply">傳送回覆</string>
<string name="action_copy_message">複製訊息</string>
<string name="action_select_message">選擇訊息</string>
<string name="action_delete_message">刪除訊息</string>
<string name="action_react_with_emoji">使用表情符號回應</string>
<string name="action_select_device">選擇裝置</string>
<string name="action_select_network">選擇網路</string>
</resources> </resources>

View file

@ -408,12 +408,9 @@
<string name="userinfo">User Info</string> <string name="userinfo">User Info</string>
<string name="meshtastic_new_nodes_notifications">New node notifications</string> <string name="meshtastic_new_nodes_notifications">New node notifications</string>
<string name="snr">SNR</string> <string name="snr">SNR</string>
<string name="snr_definition">Signal-to-Noise Ratio, a measure used in communications to quantify the level of a desired signal to the level of background noise. In Meshtastic and other wireless systems, a higher SNR indicates a clearer signal that can enhance the reliability and quality of data transmission.</string>
<string name="rssi">RSSI</string> <string name="rssi">RSSI</string>
<string name="rssi_definition">Received Signal Strength Indicator, a measurement used to determine the power level being received by the antenna. A higher RSSI value generally indicates a stronger and more stable connection.</string>
<string name="iaq_definition">(Indoor Air Quality) relative scale IAQ value as measured by Bosch BME680. Value Range 0500.</string> <string name="iaq_definition">(Indoor Air Quality) relative scale IAQ value as measured by Bosch BME680. Value Range 0500.</string>
<string name="device_metrics_log">Device Metrics</string> <string name="device_metrics_log">Device Metrics</string>
<string name="node_map">Node Map</string>
<string name="position_log">Position</string> <string name="position_log">Position</string>
<string name="last_position_update">Last position update</string> <string name="last_position_update">Last position update</string>
<string name="env_metrics_log">Environment Metrics</string> <string name="env_metrics_log">Environment Metrics</string>
@ -460,7 +457,6 @@
<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="min">Min</string>
<string name="avg">Avg</string>
<string name="expand_chart">Expand chart</string> <string name="expand_chart">Expand chart</string>
<string name="collapse_chart">Collapse chart</string> <string name="collapse_chart">Collapse chart</string>
<string name="unknown_age">Unknown Age</string> <string name="unknown_age">Unknown Age</string>
@ -638,6 +634,23 @@
<string name="ignore_mqtt">Ignore MQTT</string> <string name="ignore_mqtt">Ignore MQTT</string>
<string name="ok_to_mqtt">Ok to MQTT</string> <string name="ok_to_mqtt">Ok to MQTT</string>
<string name="mqtt_config">MQTT Config</string> <string name="mqtt_config">MQTT Config</string>
<string name="mqtt_status_inactive">Inactive</string>
<string name="mqtt_status_disconnected">Disconnected</string>
<string name="mqtt_status_disconnected_with_reason">Disconnected — %1$s</string>
<string name="mqtt_status_connecting">Connecting…</string>
<string name="mqtt_status_connected">Connected</string>
<string name="mqtt_status_reconnecting">Reconnecting…</string>
<string name="mqtt_status_reconnecting_with_attempt">Reconnecting (attempt %1$d) — %2$s</string>
<string name="mqtt_test_connection">Test connection</string>
<string name="mqtt_probe_running">Probing broker…</string>
<string name="mqtt_probe_success">Reachable. Broker accepted credentials.</string>
<string name="mqtt_probe_success_with_info">Reachable (%1$s)</string>
<string name="mqtt_probe_rejected">Broker rejected: %1$s</string>
<string name="mqtt_probe_dns_failure">Host not found</string>
<string name="mqtt_probe_tcp_failure">Cannot reach broker (TCP)</string>
<string name="mqtt_probe_tls_failure">TLS handshake failed</string>
<string name="mqtt_probe_timeout">Timed out after %1$d ms</string>
<string name="mqtt_probe_other_failure">Connection failed</string>
<string name="mqtt_enabled">MQTT enabled</string> <string name="mqtt_enabled">MQTT enabled</string>
<string name="address">Address</string> <string name="address">Address</string>
<string name="username">Username</string> <string name="username">Username</string>
@ -1265,4 +1278,18 @@
<string name="desktop_tray_show">Show Meshtastic</string> <string name="desktop_tray_show">Show Meshtastic</string>
<string name="desktop_tray_quit">Quit</string> <string name="desktop_tray_quit">Quit</string>
<string name="desktop_notification_title">Meshtastic</string> <string name="desktop_notification_title">Meshtastic</string>
<string name="export_tak_data_package">Export TAK Data Package</string>
<string name="mpwrd_os" translatable="false">mPWRD-OS</string>
<string name="clear_time_zone">Clear time zone</string>
<string name="filter_icon">Filter</string>
<string name="remove_filter">Remove filter</string>
<string name="show_iaq_legend">Show air quality legend</string>
<string name="action_show_message_status">Show message status</string>
<string name="action_send_reply">Send reply</string>
<string name="action_copy_message">Copy message</string>
<string name="action_select_message">Select message</string>
<string name="action_delete_message">Delete message</string>
<string name="action_react_with_emoji">React with emoji</string>
<string name="action_select_device">Select device</string>
<string name="action_select_network">Select network</string>
</resources> </resources>

View file

@ -0,0 +1,69 @@
/*
* 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.testing
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.repository.DeviceHardwareRepository
/**
* A test double for [DeviceHardwareRepository] backed by an in-memory map keyed by `(hwModel, target)`.
*
* Call [setHardware] (or [setHardwareForModel]) to seed results, or [setResult] to control the exact [Result] returned
* for a given lookup. By default, lookups return `Result.success(null)`.
*/
class FakeDeviceHardwareRepository :
BaseFake(),
DeviceHardwareRepository {
private val hardware = mutableMapOf<Pair<Int, String?>, Result<DeviceHardware?>>()
private val calls = mutableListOf<Triple<Int, String?, Boolean>>()
init {
registerResetAction {
hardware.clear()
calls.clear()
}
}
/** Records every [getDeviceHardwareByModel] invocation for assertion. */
val recordedCalls: List<Triple<Int, String?, Boolean>>
get() = calls.toList()
override suspend fun getDeviceHardwareByModel(
hwModel: Int,
target: String?,
forceRefresh: Boolean,
): Result<DeviceHardware?> {
calls.add(Triple(hwModel, target, forceRefresh))
return hardware[hwModel to target] ?: hardware[hwModel to null] ?: Result.success(null)
}
/** Seeds a successful lookup for the given model/target pair. */
fun setHardware(hwModel: Int, target: String? = null, device: DeviceHardware?) {
hardware[hwModel to target] = Result.success(device)
}
/** Seeds a successful lookup for any target of the given model. */
fun setHardwareForModel(hwModel: Int, device: DeviceHardware?) {
hardware[hwModel to null] = Result.success(device)
}
/** Seeds an arbitrary [Result] for the given lookup (use to test failure paths). */
fun setResult(hwModel: Int, target: String? = null, result: Result<DeviceHardware?>) {
hardware[hwModel to target] = result
}
}

View file

@ -0,0 +1,57 @@
/*
* 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.testing
import kotlinx.coroutines.flow.Flow
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.repository.FirmwareReleaseRepository
/**
* A test double for [FirmwareReleaseRepository] that exposes stable and alpha releases as
* [kotlinx.coroutines.flow.MutableStateFlow]s.
*
* Use [setStableRelease] and [setAlphaRelease] to drive the emitted values.
*/
class FakeFirmwareReleaseRepository :
BaseFake(),
FirmwareReleaseRepository {
private val _stableRelease = mutableStateFlow<FirmwareRelease?>(null)
private val _alphaRelease = mutableStateFlow<FirmwareRelease?>(null)
override val stableRelease: Flow<FirmwareRelease?> = _stableRelease
override val alphaRelease: Flow<FirmwareRelease?> = _alphaRelease
var invalidateCacheCalls: Int = 0
private set
init {
registerResetAction { invalidateCacheCalls = 0 }
}
override suspend fun invalidateCache() {
invalidateCacheCalls++
}
fun setStableRelease(release: FirmwareRelease?) {
_stableRelease.value = release
}
fun setAlphaRelease(release: FirmwareRelease?) {
_alphaRelease.value = release
}
}

View file

@ -0,0 +1,71 @@
/*
* 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.testing
import kotlinx.coroutines.flow.Flow
import org.meshtastic.core.database.entity.QuickChatAction
import org.meshtastic.core.repository.QuickChatActionRepository
/**
* A test double for [QuickChatActionRepository] that keeps actions in an in-memory list (sorted by `position`).
*
* The in-memory list is exposed reactively through [getAllActions].
*/
class FakeQuickChatActionRepository :
BaseFake(),
QuickChatActionRepository {
private val actionsFlow = mutableStateFlow<List<QuickChatAction>>(emptyList())
override fun getAllActions(): Flow<List<QuickChatAction>> = actionsFlow
override suspend fun upsert(action: QuickChatAction) {
val existingIndex = actionsFlow.value.indexOfFirst { it.uuid == action.uuid }
actionsFlow.value =
if (existingIndex >= 0) {
actionsFlow.value.toMutableList().also { it[existingIndex] = action }
} else {
actionsFlow.value + action
}
.sortedBy { it.position }
}
override suspend fun deleteAll() {
actionsFlow.value = emptyList()
}
override suspend fun delete(action: QuickChatAction) {
actionsFlow.value =
actionsFlow.value
.filterNot { it.uuid == action.uuid }
.map { if (it.position > action.position) it.copy(position = it.position - 1) else it }
}
override suspend fun setItemPosition(uuid: Long, newPos: Int) {
actionsFlow.value =
actionsFlow.value.map { if (it.uuid == uuid) it.copy(position = newPos) else it }.sortedBy { it.position }
}
/** Seeds the current list of actions (useful for test setup). */
fun setActions(actions: List<QuickChatAction>) {
actionsFlow.value = actions.sortedBy { it.position }
}
/** Returns the current in-memory snapshot. */
val currentActions: List<QuickChatAction>
get() = actionsFlow.value
}

View file

@ -0,0 +1,162 @@
/*
* 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.testing
import kotlinx.coroutines.flow.Flow
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.proto.Channel
import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.ChannelSettings
import org.meshtastic.proto.Config
import org.meshtastic.proto.DeviceProfile
import org.meshtastic.proto.DeviceUIConfig
import org.meshtastic.proto.FileInfo
import org.meshtastic.proto.LocalConfig
import org.meshtastic.proto.LocalModuleConfig
import org.meshtastic.proto.ModuleConfig
/**
* A test double for [RadioConfigRepository] backed by in-memory [kotlinx.coroutines.flow.MutableStateFlow]s.
*
* All mutator methods update the underlying state flows synchronously so tests can observe changes immediately.
* [deviceProfileFlow] is derived from [localConfigFlow], [moduleConfigFlow], and the current channel set.
*/
@Suppress("TooManyFunctions")
class FakeRadioConfigRepository :
BaseFake(),
RadioConfigRepository {
private val channelSetBacking = mutableStateFlow(ChannelSet())
override val channelSetFlow: Flow<ChannelSet> = channelSetBacking
private val localConfigBacking = mutableStateFlow(LocalConfig())
override val localConfigFlow: Flow<LocalConfig> = localConfigBacking
private val moduleConfigBacking = mutableStateFlow(LocalModuleConfig())
override val moduleConfigFlow: Flow<LocalModuleConfig> = moduleConfigBacking
private val deviceProfileBacking = mutableStateFlow(DeviceProfile())
override val deviceProfileFlow: Flow<DeviceProfile> = deviceProfileBacking
val currentDeviceProfile: DeviceProfile
get() = deviceProfileBacking.value
private val deviceUIConfigBacking = mutableStateFlow<DeviceUIConfig?>(null)
override val deviceUIConfigFlow: Flow<DeviceUIConfig?> = deviceUIConfigBacking
private val fileManifestBacking = mutableStateFlow<List<FileInfo>>(emptyList())
override val fileManifestFlow: Flow<List<FileInfo>> = fileManifestBacking
val currentChannelSet: ChannelSet
get() = channelSetBacking.value
val currentLocalConfig: LocalConfig
get() = localConfigBacking.value
val currentModuleConfig: LocalModuleConfig
get() = moduleConfigBacking.value
val currentDeviceUIConfig: DeviceUIConfig?
get() = deviceUIConfigBacking.value
val currentFileManifest: List<FileInfo>
get() = fileManifestBacking.value
/**
* Last [Config] passed to [setLocalConfig] (null until called). Tests should use [setLocalConfigDirect] to drive
* state.
*/
var lastSetLocalConfig: Config? = null
private set
/** Last [ModuleConfig] passed to [setLocalModuleConfig] (null until called). */
var lastSetModuleConfig: ModuleConfig? = null
private set
init {
registerResetAction {
lastSetLocalConfig = null
lastSetModuleConfig = null
}
}
override suspend fun clearChannelSet() {
channelSetBacking.value = ChannelSet()
}
override suspend fun replaceAllSettings(settingsList: List<ChannelSettings>) {
channelSetBacking.value = channelSetBacking.value.copy(settings = settingsList)
}
override suspend fun updateChannelSettings(channel: Channel) {
val current = channelSetBacking.value.settings.toMutableList()
while (current.size <= channel.index) current.add(ChannelSettings())
current[channel.index] = channel.settings ?: ChannelSettings()
channelSetBacking.value = channelSetBacking.value.copy(settings = current)
}
override suspend fun clearLocalConfig() {
localConfigBacking.value = LocalConfig()
}
override suspend fun setLocalConfig(config: Config) {
lastSetLocalConfig = config
}
override suspend fun clearLocalModuleConfig() {
moduleConfigBacking.value = LocalModuleConfig()
}
override suspend fun setLocalModuleConfig(config: ModuleConfig) {
lastSetModuleConfig = config
}
override suspend fun setDeviceUIConfig(config: DeviceUIConfig) {
deviceUIConfigBacking.value = config
}
override suspend fun clearDeviceUIConfig() {
deviceUIConfigBacking.value = null
}
override suspend fun addFileInfo(info: FileInfo) {
fileManifestBacking.value = fileManifestBacking.value + info
}
override suspend fun clearFileManifest() {
fileManifestBacking.value = emptyList()
}
/** Directly sets the [LocalConfig] without merging (preferred for test setup). */
fun setLocalConfigDirect(config: LocalConfig) {
localConfigBacking.value = config
}
/** Directly sets the [LocalModuleConfig] without merging (preferred for test setup). */
fun setLocalModuleConfigDirect(config: LocalModuleConfig) {
moduleConfigBacking.value = config
}
/** Directly sets the combined [DeviceProfile] emitted by [deviceProfileFlow]. */
fun setDeviceProfile(profile: DeviceProfile) {
deviceProfileBacking.value = profile
}
/** Directly sets the [ChannelSet] (bypasses [updateChannelSettings]/[replaceAllSettings]). */
fun setChannelSet(channelSet: ChannelSet) {
channelSetBacking.value = channelSet
}
}

View file

@ -0,0 +1,55 @@
/*
* 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.testing
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.meshtastic.core.repository.TracerouteSnapshotRepository
import org.meshtastic.proto.Position
/**
* A test double for [TracerouteSnapshotRepository] keyed by `logUuid`.
*
* Use [upsertSnapshotPositions] as you would in production, or [seedSnapshot] to directly inject state for a log.
*/
class FakeTracerouteSnapshotRepository :
BaseFake(),
TracerouteSnapshotRepository {
private val snapshots = mutableStateFlow<Map<String, Map<Int, Position>>>(emptyMap())
private val requestIds = mutableMapOf<String, Int>()
init {
registerResetAction { requestIds.clear() }
}
override fun getSnapshotPositions(logUuid: String): Flow<Map<Int, Position>> =
snapshots.map { it[logUuid].orEmpty() }
override suspend fun upsertSnapshotPositions(logUuid: String, requestId: Int, positions: Map<Int, Position>) {
requestIds[logUuid] = requestId
snapshots.value = snapshots.value.toMutableMap().also { it[logUuid] = positions }
}
/** Directly seeds the snapshot for a log (bypasses request-id tracking). */
fun seedSnapshot(logUuid: String, positions: Map<Int, Position>) {
snapshots.value = snapshots.value.toMutableMap().also { it[logUuid] = positions }
}
/** Returns the last request-id recorded for [logUuid], or `null` if none. */
fun lastRequestId(logUuid: String): Int? = requestIds[logUuid]
}

View file

@ -0,0 +1,129 @@
/*
* 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.testing
import app.cash.turbine.test
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.database.entity.QuickChatAction
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.proto.Channel
import org.meshtastic.proto.ChannelSettings
import org.meshtastic.proto.Position
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
class RepositoryFakesTest {
@Test
fun `FakeDeviceHardwareRepository returns seeded hardware and records calls`() = runTest {
val repo = FakeDeviceHardwareRepository()
val hw = DeviceHardware(hwModel = 42, hwModelSlug = "TEST", platformioTarget = "tlora")
repo.setHardware(hwModel = 42, target = "tlora", device = hw)
val hit = repo.getDeviceHardwareByModel(hwModel = 42, target = "tlora", forceRefresh = false)
val miss = repo.getDeviceHardwareByModel(hwModel = 99)
assertEquals(hw, hit.getOrNull())
assertNull(miss.getOrNull())
assertEquals(2, repo.recordedCalls.size)
assertEquals(Triple(42, "tlora", false), repo.recordedCalls.first())
}
@Test
fun `FakeFirmwareReleaseRepository emits stable and alpha releases`() = runTest {
val repo = FakeFirmwareReleaseRepository()
val stable = FirmwareRelease(id = "1.0", title = "1.0", pageUrl = "", zipUrl = "")
val alpha = FirmwareRelease(id = "1.1-a", title = "1.1-a", pageUrl = "", zipUrl = "")
repo.setStableRelease(stable)
repo.setAlphaRelease(alpha)
assertEquals(stable, repo.stableRelease.first())
assertEquals(alpha, repo.alphaRelease.first())
repo.invalidateCache()
repo.invalidateCache()
assertEquals(2, repo.invalidateCacheCalls)
}
@Test
fun `FakeQuickChatActionRepository upsert delete and reorder`() = runTest {
val repo = FakeQuickChatActionRepository()
val a = QuickChatAction(uuid = 1L, name = "A", message = "hi", position = 0)
val b = QuickChatAction(uuid = 2L, name = "B", message = "bye", position = 1)
repo.upsert(a)
repo.upsert(b)
assertEquals(listOf(a, b), repo.getAllActions().first())
repo.setItemPosition(uuid = 1L, newPos = 5)
assertEquals(listOf(2L, 1L), repo.getAllActions().first().map { it.uuid })
repo.delete(b)
assertEquals(1, repo.currentActions.size)
repo.deleteAll()
assertTrue(repo.currentActions.isEmpty())
}
@Test
fun `FakeQuickChatActionRepository delete compacts positions`() = runTest {
val repo = FakeQuickChatActionRepository()
val a = QuickChatAction(uuid = 1L, name = "A", message = "", position = 0)
val b = QuickChatAction(uuid = 2L, name = "B", message = "", position = 1)
val c = QuickChatAction(uuid = 3L, name = "C", message = "", position = 2)
repo.upsert(a)
repo.upsert(b)
repo.upsert(c)
repo.delete(b)
// Matches real DAO's decrementPositionsAfter: positions must stay contiguous.
assertEquals(listOf(1L to 0, 3L to 1), repo.currentActions.map { it.uuid to it.position })
}
@Test
fun `FakeTracerouteSnapshotRepository roundtrips positions keyed by log uuid`() = runTest {
val repo = FakeTracerouteSnapshotRepository()
val positions = mapOf(1 to Position(latitude_i = 10), 2 to Position(latitude_i = 20))
repo.upsertSnapshotPositions(logUuid = "log-1", requestId = 99, positions = positions)
repo.getSnapshotPositions("log-1").test { assertEquals(positions, awaitItem()) }
assertEquals(99, repo.lastRequestId("log-1"))
assertNull(repo.lastRequestId("other"))
}
@Test
fun `FakeRadioConfigRepository tracks channel set and module config`() = runTest {
val repo = FakeRadioConfigRepository()
val a = ChannelSettings(name = "A")
val b = ChannelSettings(name = "B")
repo.replaceAllSettings(listOf(a, b))
assertEquals(listOf(a, b), repo.currentChannelSet.settings)
repo.updateChannelSettings(Channel(index = 1, settings = ChannelSettings(name = "B2")))
assertEquals("B2", repo.currentChannelSet.settings[1].name)
repo.clearChannelSet()
assertTrue(repo.currentChannelSet.settings.isEmpty())
}
}

View file

@ -37,12 +37,12 @@ import androidx.lifecycle.compose.LifecycleEventEffect
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import com.eygraber.uri.toAndroidUri import com.eygraber.uri.toAndroidUri
import com.eygraber.uri.toKmpUri import com.eygraber.uri.toKmpUri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.getString
import org.meshtastic.core.common.gpsDisabled import org.meshtastic.core.common.gpsDisabled
import org.meshtastic.core.common.util.CommonUri import org.meshtastic.core.common.util.CommonUri
import org.meshtastic.core.common.util.ioDispatcher
import java.net.URLEncoder import java.net.URLEncoder
@Composable @Composable
@ -146,7 +146,7 @@ actual fun rememberReadTextFromUri(): suspend (uri: CommonUri, maxChars: Int) ->
val context = LocalContext.current val context = LocalContext.current
return remember(context) { return remember(context) {
{ uri, maxChars -> { uri, maxChars ->
withContext(Dispatchers.IO) { withContext(ioDispatcher) {
@Suppress("TooGenericExceptionCaught") @Suppress("TooGenericExceptionCaught")
try { try {
val androidUri = uri.toAndroidUri() val androidUri = uri.toAndroidUri()

View file

@ -38,6 +38,7 @@ fun ClickableTextField(
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isError: Boolean = false, isError: Boolean = false,
trailingIconContentDescription: String? = null,
) { ) {
val source = remember { MutableInteractionSource() } val source = remember { MutableInteractionSource() }
val isPressed by source.collectIsPressedAsState() val isPressed by source.collectIsPressedAsState()
@ -49,7 +50,7 @@ fun ClickableTextField(
enabled = enabled, enabled = enabled,
readOnly = true, readOnly = true,
label = { Text(stringResource(label)) }, label = { Text(stringResource(label)) },
trailingIcon = { Icon(trailingIcon, null) }, trailingIcon = { Icon(trailingIcon, trailingIconContentDescription) },
isError = isError, isError = isError,
interactionSource = source, interactionSource = source,
modifier = modifier, modifier = modifier,

View file

@ -44,6 +44,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -58,6 +59,7 @@ import org.meshtastic.core.resources.preview_gauge
import org.meshtastic.core.resources.preview_gradient import org.meshtastic.core.resources.preview_gradient
import org.meshtastic.core.resources.preview_pill import org.meshtastic.core.resources.preview_pill
import org.meshtastic.core.resources.preview_text import org.meshtastic.core.resources.preview_text
import org.meshtastic.core.resources.show_iaq_legend
import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.ThumbUp import org.meshtastic.core.ui.icon.ThumbUp
import org.meshtastic.core.ui.icon.Warning import org.meshtastic.core.ui.icon.Warning
@ -120,13 +122,18 @@ fun IndoorAirQuality(iaq: Int?, displayMode: IaqDisplayMode = IaqDisplayMode.Pil
Column { Column {
when (displayMode) { when (displayMode) {
IaqDisplayMode.Pill -> { IaqDisplayMode.Pill -> {
val legendLabel = stringResource(Res.string.show_iaq_legend)
Box( Box(
modifier = modifier =
Modifier.clip(RoundedCornerShape(10.dp)) Modifier.clip(RoundedCornerShape(10.dp))
.background(iaqEnum.color) .background(iaqEnum.color)
.width(125.dp) .width(125.dp)
.height(30.dp) .height(30.dp)
.clickable { isLegendOpen = true }, .clickable(
onClickLabel = legendLabel,
role = Role.Button,
onClick = { isLegendOpen = true },
),
) { ) {
Row( Row(
modifier = Modifier.padding(4.dp).align(Alignment.CenterStart), modifier = Modifier.padding(4.dp).align(Alignment.CenterStart),
@ -144,7 +151,15 @@ fun IndoorAirQuality(iaq: Int?, displayMode: IaqDisplayMode = IaqDisplayMode.Pil
} }
IaqDisplayMode.Dot -> { IaqDisplayMode.Dot -> {
Column(modifier = Modifier.clickable { isLegendOpen = true }) { val legendLabel = stringResource(Res.string.show_iaq_legend)
Column(
modifier =
Modifier.clickable(
onClickLabel = legendLabel,
role = Role.Button,
onClick = { isLegendOpen = true },
),
) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = "$iaq") Text(text = "$iaq")
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
@ -154,17 +169,30 @@ fun IndoorAirQuality(iaq: Int?, displayMode: IaqDisplayMode = IaqDisplayMode.Pil
} }
IaqDisplayMode.Text -> { IaqDisplayMode.Text -> {
val legendLabel = stringResource(Res.string.show_iaq_legend)
Text( Text(
text = getIaqDescriptionWithRange(iaqEnum), text = getIaqDescriptionWithRange(iaqEnum),
fontSize = 12.sp, fontSize = 12.sp,
modifier = Modifier.clickable { isLegendOpen = true }, modifier =
Modifier.clickable(
onClickLabel = legendLabel,
role = Role.Button,
onClick = { isLegendOpen = true },
),
) )
} }
IaqDisplayMode.Gauge -> { IaqDisplayMode.Gauge -> {
val legendLabel = stringResource(Res.string.show_iaq_legend)
CircularProgressIndicator( CircularProgressIndicator(
progress = { iaq / 500f }, progress = { iaq / 500f },
modifier = Modifier.size(60.dp).clickable { isLegendOpen = true }, modifier =
Modifier.size(60.dp)
.clickable(
onClickLabel = legendLabel,
role = Role.Button,
onClick = { isLegendOpen = true },
),
strokeWidth = 8.dp, strokeWidth = 8.dp,
color = iaqEnum.color, color = iaqEnum.color,
) )
@ -172,9 +200,15 @@ fun IndoorAirQuality(iaq: Int?, displayMode: IaqDisplayMode = IaqDisplayMode.Pil
} }
IaqDisplayMode.Gradient -> { IaqDisplayMode.Gradient -> {
val legendLabel = stringResource(Res.string.show_iaq_legend)
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.clickable { isLegendOpen = true }, modifier =
Modifier.clickable(
onClickLabel = legendLabel,
role = Role.Button,
onClick = { isLegendOpen = true },
),
) { ) {
LinearProgressIndicator( LinearProgressIndicator(
progress = { iaq / 500f }, progress = { iaq / 500f },

View file

@ -34,6 +34,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -80,7 +81,13 @@ fun RegularPreference(
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
} }
Column(modifier = modifier.fillMaxWidth().clickable(enabled = enabled, onClick = onClick).padding(all = 16.dp)) { Column(
modifier =
modifier
.fillMaxWidth()
.clickable(enabled = enabled, onClick = onClick, role = Role.Button)
.padding(all = 16.dp),
) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) {
FlowRow(modifier = Modifier.weight(1f), horizontalArrangement = Arrangement.SpaceBetween) { FlowRow(modifier = Modifier.weight(1f), horizontalArrangement = Arrangement.SpaceBetween) {
Text( Text(

View file

@ -20,10 +20,10 @@ package org.meshtastic.core.ui.util
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.StringResource
import org.meshtastic.core.common.util.CommonUri import org.meshtastic.core.common.util.CommonUri
import org.meshtastic.core.common.util.ioDispatcher
import java.awt.Desktop import java.awt.Desktop
import java.awt.FileDialog import java.awt.FileDialog
import java.awt.Frame import java.awt.Frame
@ -89,7 +89,7 @@ actual fun rememberOpenFileLauncher(onUriReceived: (CommonUri?) -> Unit): (mimeT
/** JVM — Reads text from a file URI. */ /** JVM — Reads text from a file URI. */
@Composable @Composable
actual fun rememberReadTextFromUri(): suspend (uri: CommonUri, maxChars: Int) -> String? = { uri, maxChars -> actual fun rememberReadTextFromUri(): suspend (uri: CommonUri, maxChars: Int) -> String? = { uri, maxChars ->
withContext(Dispatchers.IO) { withContext(ioDispatcher) {
@Suppress("TooGenericExceptionCaught") @Suppress("TooGenericExceptionCaught")
try { try {
val file = File(URI(uri.toString())) val file = File(URI(uri.toString()))

View file

@ -46,8 +46,9 @@
-keep class org.meshtastic.desktop.MainKt { *; } -keep class org.meshtastic.desktop.MainKt { *; }
# ---- Ktor Java engine (desktop-only; Android uses OkHttp) ------------------- # ---- Ktor Java engine (desktop-only; Android uses OkHttp) -------------------
# io.ktor.client.engine.java ships consumer rules; the shared
-keep class io.ktor.client.engine.java.** { *; } # HttpClientEngineFactory ServiceLoader keep in shared-rules.pro covers the
# reflective discovery path.
# ---- Meshtastic desktop host shell ------------------------------------------ # ---- Meshtastic desktop host shell ------------------------------------------

View file

@ -44,6 +44,7 @@ import org.meshtastic.core.repository.PlatformAnalytics
import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.core.repository.ServiceBroadcasts import org.meshtastic.core.repository.ServiceBroadcasts
import org.meshtastic.proto.MqttClientProxyMessage import org.meshtastic.proto.MqttClientProxyMessage
import org.meshtastic.mqtt.ConnectionState as MqttConnectionState
import org.meshtastic.proto.Position as ProtoPosition import org.meshtastic.proto.Position as ProtoPosition
/** /**
@ -162,6 +163,8 @@ class NoopMQTTRepository : MQTTRepository {
override val proxyMessageFlow: Flow<MqttClientProxyMessage> = emptyFlow() override val proxyMessageFlow: Flow<MqttClientProxyMessage> = emptyFlow()
override fun publish(topic: String, data: ByteArray, retained: Boolean) {} override fun publish(topic: String, data: ByteArray, retained: Boolean) {}
override val connectionState = MutableStateFlow<MqttConnectionState>(MqttConnectionState.Disconnected.Idle)
} }
// endregion // endregion

View file

@ -1 +1 @@
For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ Pour des notes de version détaillées, veuillez visiter : https://github.com/meshtastic/Meshtastic-Android/releases/

View file

@ -60,7 +60,14 @@ class AndroidGetDiscoveredDevicesUseCase(
override fun invoke(showMock: Boolean): Flow<DiscoveredDevices> { override fun invoke(showMock: Boolean): Flow<DiscoveredDevices> {
val nodeDb = nodeRepository.nodeDBbyNum val nodeDb = nodeRepository.nodeDBbyNum
val bondedBleFlow = bluetoothRepository.state.map { ble -> ble.bondedDevices.map { DeviceListEntry.Ble(it) } } // Filter out non-Meshtastic peripherals (headphones, cars, watches, etc.).
// BluetoothAdapter.bondedDevices returns every bonded device on the phone, so we
// must restrict the picker to entries whose advertised name matches the
// Meshtastic firmware pattern (see MeshtasticBleConstants.BLE_NAME_PATTERN).
val bondedBleFlow =
bluetoothRepository.state.map { ble ->
ble.bondedDevices.filter { it.getMeshtasticShortName() != null }.map { DeviceListEntry.Ble(it) }
}
val processedTcpFlow = val processedTcpFlow =
combine(networkRepository.resolvedList, recentAddressesDataSource.recentAddresses) { combine(networkRepository.resolvedList, recentAddressesDataSource.recentAddresses) {

View file

@ -17,13 +17,13 @@
package org.meshtastic.feature.connections.ui.components package org.meshtastic.feature.connections.ui.components
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.selectable
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
@ -41,11 +41,15 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.selected
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.action_select_device
import org.meshtastic.core.resources.add import org.meshtastic.core.resources.add
import org.meshtastic.core.resources.bluetooth import org.meshtastic.core.resources.bluetooth
import org.meshtastic.core.resources.network import org.meshtastic.core.resources.network
@ -108,11 +112,19 @@ fun DeviceListItem(
is DeviceListEntry.Mock -> stringResource(Res.string.add) is DeviceListEntry.Mock -> stringResource(Res.string.add)
} }
val selectLabel = stringResource(Res.string.action_select_device)
val isSelected = connectionState is ConnectionState.Connected
val clickableModifier = val clickableModifier =
if (onDelete != null) { if (onDelete != null) {
Modifier.combinedClickable(onClick = onSelect, onLongClick = onDelete) Modifier.semantics { selected = isSelected }
.combinedClickable(
onClickLabel = selectLabel,
role = Role.RadioButton,
onClick = onSelect,
onLongClick = onDelete,
)
} else { } else {
Modifier.clickable(onClick = onSelect) Modifier.selectable(selected = isSelected, role = Role.RadioButton, onClick = onSelect)
} }
ListItem( ListItem(

View file

@ -35,6 +35,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.StringResource
import org.koin.core.annotation.KoinViewModel 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.CommonUri
import org.meshtastic.core.common.util.safeCatching import org.meshtastic.core.common.util.safeCatching
import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.database.entity.FirmwareRelease
@ -91,6 +92,7 @@ class FirmwareUpdateViewModel(
private val firmwareUpdateManager: FirmwareUpdateManager, private val firmwareUpdateManager: FirmwareUpdateManager,
private val usbManager: FirmwareUsbManager, private val usbManager: FirmwareUsbManager,
private val fileHandler: FirmwareFileHandler, private val fileHandler: FirmwareFileHandler,
private val applicationScope: ApplicationCoroutineScope,
) : ViewModel() { ) : ViewModel() {
private val _state = MutableStateFlow<FirmwareUpdateState>(FirmwareUpdateState.Idle) private val _state = MutableStateFlow<FirmwareUpdateState>(FirmwareUpdateState.Idle)
@ -124,12 +126,10 @@ class FirmwareUpdateViewModel(
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
// viewModelScope is already cancelled when onCleared() runs, so launch cleanup in a // viewModelScope is already cancelled when onCleared() runs, so launch cleanup on the
// standalone scope. SupervisorJob prevents the coroutine from propagating failures to a // application-wide scope (SupervisorJob + ioDispatcher). NonCancellable keeps cleanup
// shared parent, and NonCancellable on the launch keeps cleanup running even if the scope // running even if something tries to cancel it mid-flight.
// is cancelled concurrently. applicationScope.launch(NonCancellable) {
@OptIn(kotlinx.coroutines.DelicateCoroutinesApi::class)
kotlinx.coroutines.GlobalScope.launch(NonCancellable) {
tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile) tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile)
} }
} }

View file

@ -108,6 +108,7 @@ class FirmwareUpdateIntegrationTest {
firmwareUpdateManager, firmwareUpdateManager,
usbManager, usbManager,
fileHandler, fileHandler,
TestApplicationCoroutineScope(testDispatcher),
) )
@Test @Test

View file

@ -124,6 +124,7 @@ class FirmwareUpdateViewModelTest {
firmwareUpdateManager, firmwareUpdateManager,
usbManager, usbManager,
fileHandler, fileHandler,
TestApplicationCoroutineScope(testDispatcher),
) )
@Test @Test

View file

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

View file

@ -116,6 +116,7 @@ class FirmwareUpdateViewModelFileTest {
firmwareUpdateManager, firmwareUpdateManager,
usbManager, usbManager,
fileHandler, fileHandler,
TestApplicationCoroutineScope(testDispatcher),
) )
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------

View file

@ -36,11 +36,18 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.action_copy_message
import org.meshtastic.core.resources.action_delete_message
import org.meshtastic.core.resources.action_react_with_emoji
import org.meshtastic.core.resources.action_select_message
import org.meshtastic.core.resources.action_send_reply
import org.meshtastic.core.resources.action_show_message_status
import org.meshtastic.core.resources.copy import org.meshtastic.core.resources.copy
import org.meshtastic.core.resources.delete import org.meshtastic.core.resources.delete
import org.meshtastic.core.resources.device_metrics_label_value import org.meshtastic.core.resources.device_metrics_label_value
@ -55,6 +62,7 @@ import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Reply import org.meshtastic.core.ui.icon.Reply
import org.meshtastic.core.ui.icon.SelectAll import org.meshtastic.core.ui.icon.SelectAll
@Suppress("LongMethod")
@Composable @Composable
fun MessageActionsContent( fun MessageActionsContent(
quickEmojis: List<String>, quickEmojis: List<String>,
@ -83,20 +91,35 @@ fun MessageActionsContent(
Text(stringResource(Res.string.device_metrics_label_value, title, statusText.orEmpty())) Text(stringResource(Res.string.device_metrics_label_value, title, statusText.orEmpty()))
}, },
leadingContent = { MessageStatusIcon(status = status) }, leadingContent = { MessageStatusIcon(status = status) },
modifier = Modifier.clickable(onClick = onStatus), modifier =
Modifier.clickable(
onClickLabel = stringResource(Res.string.action_show_message_status),
role = Role.Button,
onClick = onStatus,
),
) )
} }
ListItem( ListItem(
headlineContent = { Text(stringResource(Res.string.reply)) }, headlineContent = { Text(stringResource(Res.string.reply)) },
leadingContent = { Icon(MeshtasticIcons.Reply, contentDescription = stringResource(Res.string.reply)) }, leadingContent = { Icon(MeshtasticIcons.Reply, contentDescription = stringResource(Res.string.reply)) },
modifier = Modifier.clickable(onClick = onReply), modifier =
Modifier.clickable(
onClickLabel = stringResource(Res.string.action_send_reply),
role = Role.Button,
onClick = onReply,
),
) )
ListItem( ListItem(
headlineContent = { Text(stringResource(Res.string.copy)) }, headlineContent = { Text(stringResource(Res.string.copy)) },
leadingContent = { Icon(MeshtasticIcons.Copy, contentDescription = stringResource(Res.string.copy)) }, leadingContent = { Icon(MeshtasticIcons.Copy, contentDescription = stringResource(Res.string.copy)) },
modifier = Modifier.clickable(onClick = onCopy), modifier =
Modifier.clickable(
onClickLabel = stringResource(Res.string.action_copy_message),
role = Role.Button,
onClick = onCopy,
),
) )
ListItem( ListItem(
@ -104,13 +127,23 @@ fun MessageActionsContent(
leadingContent = { leadingContent = {
Icon(MeshtasticIcons.SelectAll, contentDescription = stringResource(Res.string.select)) Icon(MeshtasticIcons.SelectAll, contentDescription = stringResource(Res.string.select))
}, },
modifier = Modifier.clickable(onClick = onSelect), modifier =
Modifier.clickable(
onClickLabel = stringResource(Res.string.action_select_message),
role = Role.Button,
onClick = onSelect,
),
) )
ListItem( ListItem(
headlineContent = { Text(stringResource(Res.string.delete)) }, headlineContent = { Text(stringResource(Res.string.delete)) },
leadingContent = { Icon(MeshtasticIcons.Delete, contentDescription = stringResource(Res.string.delete)) }, leadingContent = { Icon(MeshtasticIcons.Delete, contentDescription = stringResource(Res.string.delete)) },
modifier = Modifier.clickable(onClick = onDelete), modifier =
Modifier.clickable(
onClickLabel = stringResource(Res.string.action_delete_message),
role = Role.Button,
onClick = onDelete,
),
) )
} }
} }
@ -130,7 +163,12 @@ private fun QuickEmojiRow(quickEmojis: List<String>, onReact: (String) -> Unit,
Modifier.size(40.dp) Modifier.size(40.dp)
.clip(CircleShape) .clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant) .background(MaterialTheme.colorScheme.surfaceVariant)
.clickable { onReact(emoji) }, .clickable(
onClickLabel = stringResource(Res.string.action_react_with_emoji),
role = Role.Button,
) {
onReact(emoji)
},
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
Text(text = emoji, style = MaterialTheme.typography.titleMedium) Text(text = emoji, style = MaterialTheme.typography.titleMedium)

View file

@ -143,7 +143,7 @@ internal fun ReactionRow(
AnimatedVisibility(emojiGroups.isNotEmpty(), modifier = modifier) { AnimatedVisibility(emojiGroups.isNotEmpty(), modifier = modifier) {
LazyRow(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { LazyRow(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
items(emojiGroups.entries.toList()) { entry -> items(emojiGroups.entries.toList(), key = { it.key }) { entry ->
val emoji = entry.key val emoji = entry.key
val reactions = entry.value val reactions = entry.value
val localReaction = reactions.find { it.user.id == DataPacket.ID_LOCAL || it.user.id == myId } val localReaction = reactions.find { it.user.id == DataPacket.ID_LOCAL || it.user.id == myId }
@ -237,7 +237,7 @@ internal fun ReactionDialog(
} }
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) { LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
items(groupedEmojis.entries.toList()) { entry -> items(groupedEmojis.entries.toList(), key = { it.key }) { entry ->
val emoji = entry.key val emoji = entry.key
val reactions = entry.value val reactions = entry.value
val localReaction = reactions.find { it.user.id == DataPacket.ID_LOCAL || it.user.id == myId } val localReaction = reactions.find { it.user.id == DataPacket.ID_LOCAL || it.user.id == myId }
@ -265,7 +265,7 @@ internal fun ReactionDialog(
HorizontalDivider(Modifier.padding(vertical = 8.dp)) HorizontalDivider(Modifier.padding(vertical = 8.dp))
LazyColumn(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(4.dp)) { LazyColumn(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(4.dp)) {
items(filteredReactions) { reaction -> items(filteredReactions, key = { reaction -> "${reaction.user.id}:${reaction.emoji}" }) { reaction ->
Column(modifier = Modifier.padding(horizontal = 8.dp)) { Column(modifier = Modifier.padding(horizontal = 8.dp)) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),

View file

@ -49,6 +49,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -56,6 +57,7 @@ import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.NodeSortOption import org.meshtastic.core.model.NodeSortOption
import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.clear
import org.meshtastic.core.resources.desc_node_filter_clear import org.meshtastic.core.resources.desc_node_filter_clear
import org.meshtastic.core.resources.node_filter_exclude_infrastructure import org.meshtastic.core.resources.node_filter_exclude_infrastructure
import org.meshtastic.core.resources.node_filter_exclude_mqtt import org.meshtastic.core.resources.node_filter_exclude_mqtt
@ -178,14 +180,19 @@ private fun NodeFilterTextField(filterText: String, onTextChange: (String) -> Un
onValueChange = onTextChange, onValueChange = onTextChange,
trailingIcon = { trailingIcon = {
if (filterText.isNotEmpty() || isFocused) { if (filterText.isNotEmpty() || isFocused) {
val clearLabel = stringResource(Res.string.clear)
Icon( Icon(
MeshtasticIcons.Close, MeshtasticIcons.Close,
contentDescription = stringResource(Res.string.desc_node_filter_clear), contentDescription = stringResource(Res.string.desc_node_filter_clear),
modifier = modifier =
Modifier.clickable { Modifier.clickable(
onTextChange("") onClickLabel = clearLabel,
focusManager.clearFocus() role = Role.Button,
}, onClick = {
onTextChange("")
focusManager.clearFocus()
},
),
) )
} }
}, },

View file

@ -43,10 +43,7 @@ internal fun handleNodeAction(
val route = viewModel.getDirectMessageRoute(menuAction.node, uiState.ourNode) val route = viewModel.getDirectMessageRoute(menuAction.node, uiState.ourNode)
navigateToMessages(route) navigateToMessages(route)
} }
is NodeMenuAction.Remove -> { is NodeMenuAction.Remove -> viewModel.handleNodeMenuAction(menuAction, onNavigateUp)
viewModel.handleNodeMenuAction(menuAction)
onNavigateUp()
}
else -> viewModel.handleNodeMenuAction(menuAction) else -> viewModel.handleNodeMenuAction(menuAction)
} }
} }

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