diff --git a/.github/renovate.json b/.github/renovate.json index 1faa1a4ad..dda9390c3 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -56,15 +56,6 @@ "changelogUrl": "https://github.com/meshtastic/protobufs/compare/{{currentDigest}}...{{newDigest}}", "automerge": true }, - { - "description": "Group CMP and the androidx.compose artifacts that track it so Renovate bumps them together (see PR #5180)", - "groupName": "compose-multiplatform", - "matchPackageNames": [ - "/^org\\.jetbrains\\.compose/", - "androidx.compose.runtime:runtime-tracing", - "androidx.compose.ui:ui-test-manifest" - ] - }, { "description": "Restrict sensitive infrastructure to manual minor updates", "matchUpdateTypes": [ diff --git a/.gitignore b/.gitignore index 447d8a28e..8447bc7f7 100644 --- a/.gitignore +++ b/.gitignore @@ -55,4 +55,3 @@ wireless-install.sh firebase-debug.log .agent_plans/ .agent_refs/ -.agent_artifacts/ diff --git a/.pr5167.diff b/.pr5167.diff deleted file mode 100644 index d0a809449..000000000 --- a/.pr5167.diff +++ /dev/null @@ -1,295 +0,0 @@ -diff --git a/core/common/src/commonMain/kotlin/org/meshtastic/core/common/di/ApplicationCoroutineScope.kt b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/di/ApplicationCoroutineScope.kt -new file mode 100644 -index 0000000000..2a27b96906 ---- /dev/null -+++ b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/di/ApplicationCoroutineScope.kt -@@ -0,0 +1,39 @@ -+/* -+ * Copyright (c) 2026 Meshtastic LLC -+ * -+ * This program is free software: you can redistribute it and/or modify -+ * it under the terms of the GNU General Public License as published by -+ * the Free Software Foundation, either version 3 of the License, or -+ * (at your option) any later version. -+ * -+ * This program is distributed in the hope that it will be useful, -+ * but WITHOUT ANY WARRANTY; without even the implied warranty of -+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -+ * GNU General Public License for more details. -+ * -+ * You should have received a copy of the GNU General Public License -+ * along with this program. If not, see . -+ */ -+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.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 . -+ */ -+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) = -- 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 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}" } - } - } diff --git a/Gemfile.lock b/Gemfile.lock index cf6a1b9c0..de497cc4a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,13 +3,13 @@ GEM specs: CFPropertyList (3.0.8) abbrev (0.1.2) - addressable (2.9.0) + addressable (2.8.8) public_suffix (>= 2.0.2, < 8.0) artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1240.0) - aws-sdk-core (3.245.0) + aws-partitions (1.1213.0) + aws-sdk-core (3.242.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -17,11 +17,11 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.123.0) - aws-sdk-core (~> 3, >= 3.244.0) + aws-sdk-kms (1.121.0) + aws-sdk-core (~> 3, >= 3.241.4) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.219.0) - aws-sdk-core (~> 3, >= 3.244.0) + aws-sdk-s3 (1.213.0) + aws-sdk-core (~> 3, >= 3.241.4) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) @@ -29,7 +29,7 @@ GEM babosa (1.0.4) base64 (0.2.0) benchmark (0.5.0) - bigdecimal (4.1.2) + bigdecimal (4.0.1) claide (1.1.0) colored (1.2) colored2 (3.1.2) @@ -68,11 +68,11 @@ GEM faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) - faraday-retry (1.0.4) + faraday-retry (1.0.3) faraday_middleware (1.2.1) faraday (~> 1.0) - fastimage (2.4.1) - fastlane (2.233.0) + fastimage (2.4.0) + fastlane (2.232.2) CFPropertyList (>= 2.3, < 4.0.0) abbrev (~> 0.1.2) addressable (>= 2.8, < 3.0.0) @@ -92,7 +92,7 @@ GEM faraday-cookie_jar (~> 0.0.6) faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) - fastlane-sirp (>= 1.1.0) + fastlane-sirp (>= 1.0.0) gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) @@ -122,9 +122,10 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.4.1) xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) - fastlane-sirp (1.1.0) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.99.0) + google-apis-androidpublisher_v3 (0.95.0) google-apis-core (>= 0.15.0, < 2.a) google-apis-core (0.18.0) addressable (~> 2.5, >= 2.5.1) @@ -138,15 +139,15 @@ GEM google-apis-core (>= 0.15.0, < 2.a) google-apis-playcustomapp_v1 (0.17.0) google-apis-core (>= 0.15.0, < 2.a) - google-apis-storage_v1 (0.61.0) + google-apis-storage_v1 (0.59.0) google-apis-core (>= 0.15.0, < 2.a) google-cloud-core (1.8.0) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (2.1.1) faraday (>= 1.0, < 3.a) - google-cloud-errors (1.6.0) - google-cloud-storage (1.59.0) + google-cloud-errors (1.5.0) + google-cloud-storage (1.58.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-core (>= 0.18, < 2) @@ -168,13 +169,13 @@ GEM httpclient (2.9.0) mutex_m jmespath (1.6.2) - json (2.19.4) + json (2.18.1) jwt (2.10.2) base64 logger (1.7.0) mini_magick (4.13.2) mini_mime (1.1.5) - multi_json (1.20.1) + multi_json (1.19.1) multipart-post (2.4.1) mutex_m (0.3.0) nanaimo (0.4.0) @@ -184,13 +185,13 @@ GEM os (1.1.4) ostruct (0.6.3) plist (3.7.2) - public_suffix (7.0.5) - rake (13.4.2) + public_suffix (7.0.2) + rake (13.3.1) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) - retriable (3.4.1) + retriable (3.1.2) rexml (3.4.4) rouge (3.28.0) ruby2_keywords (0.0.5) @@ -204,6 +205,7 @@ GEM simctl (1.6.10) CFPropertyList naturally + sysrandom (1.0.5) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index de2b3144c..14df5580d 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -40,6 +40,15 @@ -dontwarn org.bouncycastle.** -dontwarn org.openjsse.** -# Compose runtime/ui/animation/foundation/material3 keep rules now live in -# config/proguard/shared-rules.pro so both Android (R8) and desktop (ProGuard) -# get the same defence-in-depth coverage against CMP 1.11 optimizer folding. +# ---- Compose Runtime & Animation -------------------------------------------- + +# Defence-in-depth: prevent R8 tree-shaking of Compose infrastructure classes +# 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.** { *; } diff --git a/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt b/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt index 628865010..0864e55cd 100644 --- a/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt +++ b/app/src/main/kotlin/org/meshtastic/app/MainActivity.kt @@ -59,7 +59,6 @@ import org.meshtastic.app.node.metrics.getTracerouteMapOverlayInsets import org.meshtastic.app.ui.MainScreen import org.meshtastic.core.barcode.rememberBarcodeScanner import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI -import org.meshtastic.core.network.repository.UsbRepository import org.meshtastic.core.nfc.NfcScannerEffect import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.channel_invalid @@ -92,8 +91,6 @@ import org.meshtastic.feature.node.metrics.TracerouteMapScreen class MainActivity : ComponentActivity() { private val model: UIViewModel by viewModel() - private val usbRepository: UsbRepository by inject() - /** * Activity-lifecycle-aware client that binds to the mesh service. Note: This is used implicitly as it registers * itself as a LifecycleObserver in its init block. @@ -169,16 +166,6 @@ class MainActivity : ComponentActivity() { handleIntent(intent) } - override fun onResume() { - super.onResume() - // Belt-and-suspenders for the Android 12+ attach-intent quirk: if the activity is - // resumed while a USB device is already attached (e.g. process restart, returning - // from another app), the manifest-declared attach intent may have already fired - // before UsbRepository was constructed. Re-poll deviceList here so the UI reflects - // reality without requiring the user to physically replug. - usbRepository.refreshState() - } - @Composable private fun AppCompositionLocals(content: @Composable () -> Unit) { CompositionLocalProvider( @@ -270,11 +257,6 @@ class MainActivity : ComponentActivity() { UsbManager.ACTION_USB_DEVICE_ATTACHED -> { Logger.d { "USB device attached" } - // Android 12+ delivers ACTION_USB_DEVICE_ATTACHED only to manifest-declared - // receivers, so the runtime-registered UsbBroadcastReceiver inside UsbRepository - // never sees this event. Forward it explicitly so the serialDevices StateFlow - // refreshes and the device shows up in the Connect → Serial tab. - usbRepository.refreshState() showSettingsPage() } diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts index 91b8ebce2..2fa797c74 100644 --- a/build-logic/settings.gradle.kts +++ b/build-logic/settings.gradle.kts @@ -30,7 +30,7 @@ pluginManagement { } plugins { - id("com.gradle.develocity") version("4.4.1") + id("com.gradle.develocity") version("4.4.0") } dependencyResolutionManagement { diff --git a/config/proguard/shared-rules.pro b/config/proguard/shared-rules.pro index 8d0d8efde..902636dbf 100644 --- a/config/proguard/shared-rules.pro +++ b/config/proguard/shared-rules.pro @@ -20,10 +20,12 @@ -keepattributes SourceFile,LineNumberTable,*Annotation*,Signature,InnerClasses,EnclosingMethod,Exceptions,RuntimeVisibleAnnotations # ---- Kotlin / Coroutines ---------------------------------------------------- -# Kotlin stdlib and kotlinx-coroutines ship their own consumer ProGuard rules -# (kotlin-stdlib and kotlinx-coroutines-core consumer-rules.pro) which keep -# Metadata, Continuation, kotlin.reflect internals, and debug metadata. No -# explicit wildcards needed here. + +-keep class kotlin.Metadata { *; } +-keep class kotlin.reflect.** { *; } +-keep class kotlin.coroutines.Continuation { *; } +-keep class kotlinx.coroutines.** { *; } +-dontwarn kotlinx.coroutines.** # ---- Koin DI (reflection-based injection) ----------------------------------- @@ -39,7 +41,9 @@ -keep @org.koin.core.annotation.ComponentScan class * { *; } -keep @org.koin.core.annotation.Single class * { *; } -keep @org.koin.core.annotation.Factory class * { *; } --keep @org.koin.core.annotation.KoinViewModel class * { *; } + +# Generated Koin module extensions (Koin Annotations plugin output) +-keep class org.meshtastic.**.di.** { *; } # ---- kotlinx-serialization -------------------------------------------------- @@ -59,14 +63,13 @@ # ---- Wire Protobuf ---------------------------------------------------------- -# Wire generates an ADAPTER static field on every Message subclass accessed -# reflectively during encoding/decoding. Keep those fields and the -# ProtoAdapter subclasses themselves; Wire's bundled consumer rules preserve -# the runtime itself. --keepclassmembers class * extends com.squareup.wire.Message { - public static *** ADAPTER; -} --keepclassmembers class * extends com.squareup.wire.ProtoAdapter { *; } +# Wire generates ADAPTER companion objects accessed via reflection +-keep class com.squareup.wire.** { *; } +-dontwarn com.squareup.wire.** + +# Generated proto message classes (both meshtastic protos and internal package) +-keep class org.meshtastic.proto.** { *; } +-keep class meshtastic.** { *; } # Suppress warnings about missing Android Parcelable (Wire cross-platform stubs # when compiling for non-Android JVM targets; harmless on Android). @@ -83,24 +86,40 @@ -keep class org.meshtastic.core.database.MeshtasticDatabaseConstructor { *; } -keep class org.meshtastic.core.database.MeshtasticDatabase { *; } -# Room's own consumer rules (from androidx.room3) keep DAOs, entities, -# generated _Impl classes, and TypeConverters referenced from the database. +# Room DAOs — Room generates implementations at compile time; keep interfaces +-keep class org.meshtastic.core.database.dao.** { *; } + +# 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 -------------------------------------------------------- -# androidx.sqlite ships consumer rules. + +-keep class androidx.sqlite.** { *; } +-dontwarn androidx.sqlite.** # ---- Ktor (ServiceLoader + plugin discovery) -------------------------------- -# Keep ServiceLoader metadata files (ktor discovers HttpClientEngineFactory -# implementations reflectively via ServiceLoader). +-keep class io.ktor.** { *; } +-dontwarn io.ktor.** + +# Keep ServiceLoader metadata files -keepclassmembers class * implements io.ktor.client.HttpClientEngineFactory { *; } # ---- Coil 3 (image loading) ------------------------------------------------- -# coil3 ships consumer rules. + +-keep class coil3.** { *; } +-dontwarn coil3.** # ---- Kable BLE -------------------------------------------------------------- -# com.juul.kable ships consumer rules; if release builds fail with missing -# Kable classes, restore a narrow keep for the specific reflection-loaded type. + +-keep class com.juul.kable.** { *; } +-dontwarn com.juul.kable.** # ---- Compose Multiplatform resources ---------------------------------------- @@ -108,14 +127,17 @@ # Without these the fdroid flavor has crashed at startup with a misleading # URLDecodeException due to R8 exception-class merging. -keep class org.jetbrains.compose.resources.** { *; } --keep class org.meshtastic.core.resources.Res { *; } --keepclassmembers class org.meshtastic.core.resources.Res$* { *; } +-keep class org.meshtastic.core.resources.** { *; } # ---- AboutLibraries --------------------------------------------------------- -# com.mikepenz.aboutlibraries ships consumer rules. + +-keep class com.mikepenz.aboutlibraries.** { *; } +-dontwarn com.mikepenz.aboutlibraries.** # ---- Multiplatform Markdown Renderer ---------------------------------------- -# com.mikepenz.markdown ships consumer rules. + +-keep class com.mikepenz.markdown.** { *; } +-dontwarn com.mikepenz.markdown.** # ---- QR Code Kotlin --------------------------------------------------------- @@ -125,42 +147,33 @@ -dontwarn qrcode.** # ---- Kermit logging --------------------------------------------------------- -# co.touchlab.kermit ships consumer rules. + +-keep class co.touchlab.kermit.** { *; } +-dontwarn co.touchlab.kermit.** # ---- Okio ------------------------------------------------------------------- -# okio ships consumer rules. + +-keep class okio.** { *; } +-dontwarn okio.** # ---- DataStore -------------------------------------------------------------- -# androidx.datastore ships consumer rules. + +-keep class androidx.datastore.** { *; } +-dontwarn androidx.datastore.** # ---- Paging ----------------------------------------------------------------- -# androidx.paging ships consumer rules. + +-keep class androidx.paging.** { *; } +-dontwarn androidx.paging.** # ---- 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 ------------------------------------------------ -# core.model types are reached via static references from Koin-wired graphs, -# Room entities, and kotlinx-serialization @Serializable companions — all of -# which have their own keep rules above. -# ---- Compose Runtime & Animation -------------------------------------------- - -# Defence-in-depth: prevent tree-shaking of Compose infrastructure classes that -# are referenced indirectly through compiler-generated state machines. Applies -# to BOTH R8 (Android app) and ProGuard (desktop distribution). -# -# Why shared: CMP 1.11 ships consumer rules with -assumenosideeffects on -# Composer.() / ComposerImpl.() 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.** { *; } +# Core model classes (used in serialization, Room, and Koin injection) +-keep class org.meshtastic.core.model.** { *; } diff --git a/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleExceptionClassifier.kt b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleExceptionClassifier.kt index d273a0b90..6f5180b60 100644 --- a/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleExceptionClassifier.kt +++ b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleExceptionClassifier.kt @@ -26,9 +26,7 @@ import com.juul.kable.UnmetRequirementException /** * Classification of a BLE-layer exception for the transport layer to act on. * - * @property isPermanent `true` if the condition cannot resolve without explicit user re-selection of the device. - * Currently always `false` — all known BLE exceptions can resolve without user intervention (BT toggling, permission - * grants, transient GATT errors). Reserved for future use. + * @property isPermanent `true` if the condition won't resolve without user intervention (e.g. Bluetooth disabled). * @property gattStatus the platform GATT status code when available (Android-specific). * @property message a human-readable description of the failure. */ @@ -52,9 +50,6 @@ fun Throwable.classifyBleException(): BleExceptionInfo? = when (this) { is GattRequestRejectedException -> BleExceptionInfo(isPermanent = false, message = "GATT request rejected (busy)") is UnmetRequirementException -> - // Bluetooth disabled or runtime permission missing. Both can resolve without re-selecting the - // device (user re-enables BT, or grants permission). Surface as transient so the transport keeps - // retrying; UI can show a hint based on the message. - BleExceptionInfo(isPermanent = false, message = message ?: "Bluetooth LE unavailable") + BleExceptionInfo(isPermanent = true, message = message ?: "Bluetooth LE unavailable") else -> null } 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 deleted file mode 100644 index 2a27b9690..000000000 --- a/core/common/src/commonMain/kotlin/org/meshtastic/core/common/di/ApplicationCoroutineScope.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -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/common/src/commonMain/kotlin/org/meshtastic/core/common/util/MetricFormatter.kt b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/MetricFormatter.kt index 51905ff41..8e57b4dbb 100644 --- a/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/MetricFormatter.kt +++ b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/MetricFormatter.kt @@ -23,7 +23,6 @@ package org.meshtastic.core.common.util * All methods return locale-independent strings using [NumberFormatter] (dot decimal separator), which is intentional * for a mesh networking app where consistency matters. */ -@Suppress("TooManyFunctions") object MetricFormatter { fun temperature(celsius: Float, isFahrenheit: Boolean): String { @@ -48,12 +47,6 @@ object MetricFormatter { fun snr(value: Float, decimalPlaces: Int = 1): String = "${NumberFormatter.format(value, decimalPlaces)} dB" fun rssi(value: Int): String = "$value dBm" - - fun windSpeed(metersPerSecond: Float, decimalPlaces: Int = 1): String = - "${NumberFormatter.format(metersPerSecond, decimalPlaces)} m/s" - - fun rainfall(millimeters: Float, decimalPlaces: Int = 1): String = - "${NumberFormatter.format(millimeters, decimalPlaces)} mm" } private const val FAHRENHEIT_SCALE = 1.8f diff --git a/core/common/src/commonTest/kotlin/org/meshtastic/core/common/util/MetricFormatterTest.kt b/core/common/src/commonTest/kotlin/org/meshtastic/core/common/util/MetricFormatterTest.kt index 94781fca3..b602a4a62 100644 --- a/core/common/src/commonTest/kotlin/org/meshtastic/core/common/util/MetricFormatterTest.kt +++ b/core/common/src/commonTest/kotlin/org/meshtastic/core/common/util/MetricFormatterTest.kt @@ -120,24 +120,4 @@ class MetricFormatterTest { fun snrNegative() { assertEquals("-5.5 dB", MetricFormatter.snr(-5.5f)) } - - @Test - fun windSpeed() { - assertEquals("12.3 m/s", MetricFormatter.windSpeed(12.34f)) - } - - @Test - fun windSpeedZero() { - assertEquals("0.0 m/s", MetricFormatter.windSpeed(0.0f)) - } - - @Test - fun rainfall() { - assertEquals("2.5 mm", MetricFormatter.rainfall(2.54f)) - } - - @Test - fun rainfallZero() { - assertEquals("0.0 mm", MetricFormatter.rainfall(0.0f)) - } } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt index 022f3548d..a60dc85c5 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt @@ -60,7 +60,6 @@ import org.meshtastic.proto.AdminMessage import org.meshtastic.proto.Config import org.meshtastic.proto.Telemetry import org.meshtastic.proto.ToRadio -import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds import kotlin.time.DurationUnit @@ -212,11 +211,11 @@ class MeshConnectionManagerImpl( } } - private fun startHandshakeStallGuard(stage: Int, timeout: Duration, action: () -> Unit) { + private fun startHandshakeStallGuard(stage: Int, action: () -> Unit) { handshakeTimeout?.cancel() handshakeTimeout = scope.handledLaunch { - delay(timeout) + delay(HANDSHAKE_TIMEOUT) if (serviceRepository.connectionState.value is ConnectionState.Connecting) { // Attempt one retry. Note: the firmware silently drops identical consecutive // writes (per-connection dedup). If the first want_config_id was received and @@ -292,13 +291,13 @@ class MeshConnectionManagerImpl( override fun startConfigOnly() { val action = { packetHandler.sendToRadio(ToRadio(want_config_id = HandshakeConstants.CONFIG_NONCE)) } - startHandshakeStallGuard(1, HANDSHAKE_TIMEOUT_STAGE1, action) + startHandshakeStallGuard(1, action) action() } override fun startNodeInfoOnly() { val action = { packetHandler.sendToRadio(ToRadio(want_config_id = HandshakeConstants.NODE_INFO_NONCE)) } - startHandshakeStallGuard(2, HANDSHAKE_TIMEOUT_STAGE2, action) + startHandshakeStallGuard(2, action) action() } @@ -405,14 +404,7 @@ class MeshConnectionManagerImpl( */ private const val PRE_HANDSHAKE_SETTLE_MS = 100L - private val HANDSHAKE_TIMEOUT_STAGE1 = 30.seconds - - /** - * Stage 2 drains the full node database, which can be significantly larger than Stage 1 config on big meshes. - * 60 s matches the meshtastic-client SDK timeout and avoids premature stall-guard triggers on meshes with 50+ - * nodes. - */ - private val HANDSHAKE_TIMEOUT_STAGE2 = 60.seconds + private val HANDSHAKE_TIMEOUT = 30.seconds // Shorter window for the retry attempt: if the device genuinely didn't receive the // first want_config_id the retry completes within a few seconds. Waiting another 30s diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt index d9d21ad8b..7a6ec3320 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt @@ -32,8 +32,6 @@ import org.meshtastic.core.common.util.nowSeconds import org.meshtastic.core.model.MeshLog import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.isLora -import org.meshtastic.core.model.util.toOneLineString -import org.meshtastic.core.model.util.toPIIString import org.meshtastic.core.repository.FromRadioPacketHandler import org.meshtastic.core.repository.MeshLogRepository import org.meshtastic.core.repository.MeshMessageProcessor @@ -127,11 +125,11 @@ class MeshMessageProcessorImpl( proto.xmodemPacket != null -> "XmodemPacket" to proto.xmodemPacket.toString() proto.deviceuiConfig != null -> "DeviceUIConfig" to proto.deviceuiConfig.toString() proto.fileInfo != null -> "FileInfo" to proto.fileInfo.toString() - proto.my_info != null -> "MyInfo" to proto.my_info!!.toOneLineString() - proto.node_info != null -> "NodeInfo" to proto.node_info!!.toPIIString() - proto.config != null -> "Config" to proto.config!!.toOneLineString() - proto.moduleConfig != null -> "ModuleConfig" to proto.moduleConfig!!.toOneLineString() - proto.channel != null -> "Channel" to proto.channel!!.toOneLineString() + proto.my_info != null -> "MyInfo" to proto.my_info.toString() + proto.node_info != null -> "NodeInfo" to proto.node_info.toString() + proto.config != null -> "Config" to proto.config.toString() + proto.moduleConfig != null -> "ModuleConfig" to proto.moduleConfig.toString() + proto.channel != null -> "Channel" to proto.channel.toString() proto.clientNotification != null -> "ClientNotification" to proto.clientNotification.toString() else -> return } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt index 5693d343b..b928e8505 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt @@ -20,28 +20,15 @@ import co.touchlab.kermit.Logger import co.touchlab.kermit.Severity import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.stateIn import org.koin.core.annotation.Named import org.koin.core.annotation.Single -import org.meshtastic.core.model.MqttConnectionState -import org.meshtastic.core.model.MqttProbeStatus import org.meshtastic.core.network.repository.MQTTRepository -import org.meshtastic.core.network.repository.resolveEndpoint import org.meshtastic.core.repository.MqttManager import org.meshtastic.core.repository.PacketHandler import org.meshtastic.core.repository.ServiceRepository -import org.meshtastic.mqtt.ConnectionState -import org.meshtastic.mqtt.MqttClient -import org.meshtastic.mqtt.MqttException -import org.meshtastic.mqtt.ProbeResult -import org.meshtastic.mqtt.probe import org.meshtastic.proto.MqttClientProxyMessage import org.meshtastic.proto.ToRadio @@ -53,30 +40,18 @@ class MqttManagerImpl( @Named("ServiceScope") private val scope: CoroutineScope, ) : MqttManager { private var mqttMessageFlow: Job? = null - private val proxyActive = MutableStateFlow(false) - - override val mqttConnectionState: StateFlow = - combine(proxyActive, mqttRepository.connectionState) { active, libState -> - if (!active) MqttConnectionState.Inactive else libState.toAppState() - } - .stateIn(scope, SharingStarted.Eagerly, MqttConnectionState.Inactive) override fun startProxy(enabled: Boolean, proxyToClientEnabled: Boolean) { if (mqttMessageFlow?.isActive == true) return if (enabled && proxyToClientEnabled) { - proxyActive.value = true mqttMessageFlow = mqttRepository.proxyMessageFlow .onEach { message -> packetHandler.sendToRadio(ToRadio(mqttClientProxyMessage = message)) } .catch { throwable -> - proxyActive.value = false - val message = - when (throwable) { - is MqttException.ConnectionRejected -> "MQTT: connection rejected (check credentials)" - is MqttException.ConnectionLost -> "MQTT: connection lost" - else -> "MQTT proxy failed: ${throwable.message}" - } - serviceRepository.setErrorMessage(text = message, severity = Severity.Warn) + serviceRepository.setErrorMessage( + text = "MqttClientProxy failed: $throwable", + severity = Severity.Warn, + ) } .launchIn(scope) } @@ -88,7 +63,6 @@ class MqttManagerImpl( mqttMessageFlow?.cancel() mqttMessageFlow = null } - proxyActive.value = false } override fun handleMqttProxyMessage(message: MqttClientProxyMessage) { @@ -105,57 +79,4 @@ class MqttManagerImpl( else -> {} } } - - private fun ConnectionState.toAppState(): MqttConnectionState = when (this) { - is ConnectionState.Connecting -> MqttConnectionState.Connecting - is ConnectionState.Connected -> MqttConnectionState.Connected - is ConnectionState.Reconnecting -> - MqttConnectionState.Reconnecting(attempt = attempt, lastError = lastError?.message) - is ConnectionState.Disconnected -> - reason?.let { MqttConnectionState.Disconnected(reason = it.message) } - ?: MqttConnectionState.Disconnected.Idle - } - - override suspend fun probe( - address: String, - tlsEnabled: Boolean, - username: String?, - password: String?, - ): MqttProbeStatus { - val endpoint = resolveEndpoint(address, tlsEnabled) - val result = - MqttClient.probe(endpoint = endpoint) { - val user = username?.takeUnless { it.isEmpty() } - val pass = password?.takeUnless { it.isEmpty() } - if (user != null) this.username = user - if (pass != null) password(pass) - } - return result.toAppStatus() - } - - private fun ProbeResult.toAppStatus(): MqttProbeStatus = when (this) { - is ProbeResult.Success -> { - val info = serverInfo - val summary = - buildList { - info.assignedClientIdentifier?.let { add("client=$it") } - info.maximumQosOrdinal?.let { add("maxQoS=$it") } - info.serverKeepAliveSeconds?.let { add("keepalive=${it}s") } - } - .joinToString(", ") - .ifEmpty { null } - MqttProbeStatus.Success(serverInfo = summary) - } - is ProbeResult.Rejected -> - MqttProbeStatus.Rejected( - reasonCode = reasonCode.value, - reason = message, - serverReference = serverReference, - ) - is ProbeResult.DnsFailure -> MqttProbeStatus.DnsFailure(message = cause.message) - is ProbeResult.TcpFailure -> MqttProbeStatus.TcpFailure(message = cause.message) - is ProbeResult.TlsFailure -> MqttProbeStatus.TlsFailure(message = cause.message) - is ProbeResult.Timeout -> MqttProbeStatus.Timeout(timeoutMs = durationMs) - is ProbeResult.Other -> MqttProbeStatus.Other(message = cause.message) - } } diff --git a/core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/MigrationTest.kt b/core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/MigrationTest.kt index 451a62174..8062afa76 100644 --- a/core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/MigrationTest.kt +++ b/core/database/src/androidHostTest/kotlin/org/meshtastic/core/database/dao/MigrationTest.kt @@ -20,7 +20,7 @@ import androidx.room3.Room import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import kotlinx.coroutines.flow.first -import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.runBlocking import okio.ByteString.Companion.toByteString import org.junit.After import org.junit.Before @@ -59,7 +59,7 @@ class MigrationTest { ) @Before - fun createDb(): Unit = runTest { + fun createDb(): Unit = runBlocking { val context = ApplicationProvider.getApplicationContext() database = Room.inMemoryDatabaseBuilder( @@ -77,7 +77,7 @@ class MigrationTest { } @Test - fun testMigrateChannelsByPSK_duplicatePSK() = runTest { + fun testMigrateChannelsByPSK_duplicatePSK() = runBlocking { // PSK \"AQ==\" is base64 for single byte 0x01 val pskBytes = byteArrayOf(0x01).toByteString() @@ -103,7 +103,7 @@ class MigrationTest { } @Test - fun testMigrateChannelsByPSK_reorder() = runTest { + fun testMigrateChannelsByPSK_reorder() = runBlocking { val pskA = byteArrayOf(0x01).toByteString() val pskB = byteArrayOf(0x02).toByteString() @@ -122,7 +122,7 @@ class MigrationTest { } @Test - fun testMigrateChannelsByPSK_disambiguateByName() = runTest { + fun testMigrateChannelsByPSK_disambiguateByName() = runBlocking { val pskA = byteArrayOf(0x01).toByteString() insertPacket(channel = 0, text = "Msg A1") @@ -141,7 +141,7 @@ class MigrationTest { } @Test - fun testMigrateChannelsByPSK_preferSameIndexIfStillAmbiguous() = runTest { + fun testMigrateChannelsByPSK_preferSameIndexIfStillAmbiguous() = runBlocking { val pskA = byteArrayOf(0x01).toByteString() insertPacket(channel = 0, text = "Msg A") diff --git a/core/model/consumer-rules.pro b/core/model/consumer-rules.pro new file mode 100644 index 000000000..5f75d687d --- /dev/null +++ b/core/model/consumer-rules.pro @@ -0,0 +1,2 @@ +-keep class org.meshtastic.core.model.DataPacket +-keep class org.meshtastic.core.model.DataPacket$CREATOR diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MqttConnectionState.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MqttConnectionState.kt deleted file mode 100644 index 4d3bfca10..000000000 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MqttConnectionState.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2025-2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -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) - } - } -} diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MqttProbeStatus.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MqttProbeStatus.kt deleted file mode 100644 index e3cb7c77a..000000000 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/MqttProbeStatus.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -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() -} diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/Extensions.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/Extensions.kt index dfe70fd92..47d812f68 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/Extensions.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/util/Extensions.kt @@ -18,11 +18,8 @@ package org.meshtastic.core.model.util -import org.meshtastic.proto.Channel import org.meshtastic.proto.Config import org.meshtastic.proto.MeshPacket -import org.meshtastic.proto.ModuleConfig -import org.meshtastic.proto.MyNodeInfo import org.meshtastic.proto.Telemetry /** @@ -51,24 +48,6 @@ fun MeshPacket.toOneLineString(): String { return this.toString().replace(redactedFields.toRegex()) { "${it.groupValues[1]}=[REDACTED]" }.replace('\n', ' ') } -fun Channel.toOneLineString(): String { - // Redact the channel preshared key (psk) from logs. - val redactedFields = """(psk)=[^,}]+""" - return this.toString().replace(redactedFields.toRegex()) { "${it.groupValues[1]}=[REDACTED]" }.replace('\n', ' ') -} - -fun ModuleConfig.toOneLineString(): String { - // Redact MQTT credentials from logs. - val redactedFields = """(password|username)=[^,}]+""" - return this.toString().replace(redactedFields.toRegex()) { "${it.groupValues[1]}=[REDACTED]" }.replace('\n', ' ') -} - -fun MyNodeInfo.toOneLineString(): String { - // Redact the hardware unique identifier from logs. - val redactedFields = """(device_id)=[^,}]+""" - return this.toString().replace(redactedFields.toRegex()) { "${it.groupValues[1]}=[REDACTED]" }.replace('\n', ' ') -} - fun Any.toPIIString() = if (!isDebug) { "" } else { diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index f2fb85d7f..c3dc2ffd5 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -40,7 +40,8 @@ kotlin { implementation(projects.core.ble) implementation(libs.okio) - api(libs.meshtastic.mqtt.client) + implementation(libs.kmqtt.client) + implementation(libs.kmqtt.common) implementation(libs.kotlinx.serialization.json) implementation(libs.ktor.client.core) implementation(libs.ktor.client.content.negotiation) diff --git a/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/SerialRadioTransport.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/SerialRadioTransport.kt index 0f7985276..bc3558800 100644 --- a/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/SerialRadioTransport.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/SerialRadioTransport.kt @@ -108,10 +108,7 @@ class SerialRadioTransport( "Uptime: ${uptime}ms, " + "Packets RX: $packetsReceived ($bytesReceived bytes)" } - // USB unplug / cable error is transient — the transport will reconnect when - // the device is replugged or the OS re-enumerates the port. Only an explicit - // close() (user disconnects) should signal a permanent disconnect. - onDeviceDisconnect(waitForStopped = false, isPermanent = false) + onDeviceDisconnect(false) } }, ) diff --git a/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/SerialConnectionImpl.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/SerialConnectionImpl.kt index d8b14be03..b2ccf6545 100644 --- a/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/SerialConnectionImpl.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/SerialConnectionImpl.kt @@ -87,11 +87,6 @@ internal class SerialConnectionImpl( port.open(usbDeviceConnection) port.setParameters(115200, UsbSerialPort.DATABITS_8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE) - - // Assert DTR/RTS so native USB-CDC firmware (RAK4631 / nRF52840) recognizes the host as - // present and starts its serial-side Meshtastic protocol. Empirically, omitting these - // signals causes the firmware to never respond to WAKE_BYTES, stalling the handshake at - // Stage 1. Bridge-chip boards (CH340, CP210x, FTDI) tolerate the assertion. port.dtr = true port.rts = true diff --git a/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/UsbRepository.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/UsbRepository.kt index c5080ec14..b4773dff3 100644 --- a/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/UsbRepository.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/UsbRepository.kt @@ -54,7 +54,9 @@ class UsbRepository( _serialDevices .mapLatest { serialDevices -> val serialProber = usbSerialProberLazy.value - buildMap { serialDevices.forEach { (k, v) -> serialProber.probeDevice(v)?.let { put(k, it) } } } + buildMap { + serialDevices.forEach { (k, v) -> serialProber.probeDevice(v)?.let { driver -> put(k, driver) } } + } } .stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, emptyMap()) @@ -81,8 +83,6 @@ class UsbRepository( processLifecycle.coroutineScope.launch(dispatchers.default) { refreshStateInternal() } } - private suspend fun refreshStateInternal() = withContext(dispatchers.default) { - val devices = usbManagerLazy.value?.deviceList ?: emptyMap() - _serialDevices.emit(devices) - } + private suspend fun refreshStateInternal() = + withContext(dispatchers.default) { _serialDevices.emit(usbManagerLazy.value?.deviceList ?: emptyMap()) } } diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/BleRadioTransport.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/BleRadioTransport.kt index f2ba25804..77114ff55 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/BleRadioTransport.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/BleRadioTransport.kt @@ -133,11 +133,7 @@ class BleRadioTransport( @Volatile private var isFullyConnected = false private var connectionJob: Job? = null - - // Never give up while the user has this device selected. Higher layers (SharedRadioInterfaceService) - // own the explicit-disconnect lifecycle and will close() us when the user picks a different device or - // toggles the connection off; until then, retry forever with the policy's exponential-backoff cap (60 s). - private val reconnectPolicy = BleReconnectPolicy(maxFailures = Int.MAX_VALUE) + private val reconnectPolicy = BleReconnectPolicy() private val heartbeatSender = HeartbeatSender( diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/BleReconnectPolicy.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/BleReconnectPolicy.kt index e4d250796..cef746af0 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/BleReconnectPolicy.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/BleReconnectPolicy.kt @@ -26,11 +26,10 @@ import kotlin.time.Duration.Companion.seconds /** * Encapsulates the BLE reconnection policy with exponential backoff. * - * The policy tracks consecutive failures and decides whether to retry or signal a transient disconnect (DeviceSleep). - * When [maxFailures] is reached the [execute] loop invokes [execute]'s `onPermanentDisconnect` callback and returns; - * set [maxFailures] to [Int.MAX_VALUE] (as [BleRadioTransport] does) to disable the give-up path entirely. + * The policy tracks consecutive failures and decides whether to retry, signal a transient disconnect (DeviceSleep), or + * give up permanently. * - * @param maxFailures maximum consecutive failures before giving up; use [Int.MAX_VALUE] to retry indefinitely + * @param maxFailures maximum consecutive failures before giving up permanently * @param failureThreshold after this many consecutive failures, signal a transient disconnect * @param settleDelay delay before each connection attempt to let the BLE stack settle * @param minStableConnection minimum time a connection must stay up to be considered "stable" @@ -149,18 +148,7 @@ class BleReconnectPolicy( companion object { const val DEFAULT_MAX_FAILURES = 10 const val DEFAULT_FAILURE_THRESHOLD = 3 - - /** - * Delay applied before every connection attempt (including the first) so the BLE stack and the firmware-side - * GATT session have time to settle. - * - * Empirically validated against the meshtastic-client KMP SDK probes (Apr 2026): with a 1.5 s pause between - * disconnect→reconnect cycles, 3/5–4/5 attempts failed mid-handshake (Stage1Draining timeouts) because the - * firmware had not yet released its GATT session from the previous cycle. With ≥ 5 s pause, success rate rose - * to 5/5 against a strong (-53 dBm) link. 3 s is a conservative compromise on Android, whose BLE stack is more - * mature than btleplug+CoreBluetooth, but the firmware-side cleanup constraint is the same. - */ - val DEFAULT_SETTLE_DELAY = 3.seconds + val DEFAULT_SETTLE_DELAY = 1.seconds val DEFAULT_MIN_STABLE_CONNECTION = 5.seconds internal val RECONNECT_BASE_DELAY = 5.seconds diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/StreamTransport.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/StreamTransport.kt index 8c689dbcb..ac912346a 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/StreamTransport.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/StreamTransport.kt @@ -37,20 +37,18 @@ abstract class StreamTransport(protected val callback: RadioTransportCallback, p override suspend fun close() { Logger.d { "Closing stream for good" } - onDeviceDisconnect(waitForStopped = true, isPermanent = true) + onDeviceDisconnect(true) } /** - * Signals the transport callback that the device has disconnected and optionally waits for the transport to stop. + * Notify the transport callback that our device has gone away, but wait for it to come back. * * @param waitForStopped if true we should wait for the transport to finish - must be false if called from inside * transport callbacks - * @param isPermanent true only when the user has explicitly disconnected (e.g. [close] was called). USB unplug, I/O - * errors, and similar conditions are transient — the transport may recover when the device is replugged or the OS - * re-enumerates. Defaults to false so callbacks default to "may come back"; [close] passes true explicitly to - * signal a user-initiated terminal disconnect. + * @param isPermanent true if the device is definitely gone (e.g. USB unplugged), false if it may come back (e.g. + * TCP transient disconnect). Defaults to true for serial — subclasses may override with false. */ - protected open fun onDeviceDisconnect(waitForStopped: Boolean, isPermanent: Boolean = false) { + protected open fun onDeviceDisconnect(waitForStopped: Boolean, isPermanent: Boolean = true) { callback.onDisconnect(isPermanent = isPermanent) } diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepository.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepository.kt index 9efb9150b..fe092fd7c 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepository.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepository.kt @@ -17,8 +17,6 @@ package org.meshtastic.core.network.repository import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow -import org.meshtastic.mqtt.ConnectionState import org.meshtastic.proto.MqttClientProxyMessage /** Interface defining the MQTT interactions used for proxying messages to and from the mesh. */ @@ -40,7 +38,4 @@ interface MQTTRepository { * @param retained Whether the message should be retained by the broker. */ fun publish(topic: String, data: ByteArray, retained: Boolean) - - /** Observable MQTT connection lifecycle state (DISCONNECTED → CONNECTING → CONNECTED → RECONNECTING). */ - val connectionState: StateFlow } diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt index 47cfb6f7a..5e4ffa91d 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt @@ -17,15 +17,22 @@ package org.meshtastic.core.network.repository import co.touchlab.kermit.Logger +import io.github.davidepianca98.MQTTClient +import io.github.davidepianca98.mqtt.MQTTException +import io.github.davidepianca98.mqtt.MQTTVersion +import io.github.davidepianca98.mqtt.Subscription +import io.github.davidepianca98.mqtt.packets.Qos +import io.github.davidepianca98.mqtt.packets.mqttv5.ReasonCode +import io.github.davidepianca98.mqtt.packets.mqttv5.SubscriptionOptions +import io.github.davidepianca98.socket.IOException +import io.github.davidepianca98.socket.tls.TLSClientSettings +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.channels.ProducerScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -37,19 +44,11 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonDecodingException import okio.ByteString.Companion.toByteString import org.koin.core.annotation.Single -import org.meshtastic.core.common.util.safeCatching import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.MqttJsonPayload import org.meshtastic.core.model.util.subscribeList import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.RadioConfigRepository -import org.meshtastic.mqtt.ConnectionState -import org.meshtastic.mqtt.MqttClient -import org.meshtastic.mqtt.MqttEndpoint -import org.meshtastic.mqtt.MqttException -import org.meshtastic.mqtt.MqttMessage -import org.meshtastic.mqtt.QoS -import org.meshtastic.mqtt.packet.Subscription import org.meshtastic.proto.MqttClientProxyMessage import kotlin.concurrent.Volatile @@ -65,16 +64,12 @@ class MQTTRepositoryImpl( private const val DEFAULT_TOPIC_LEVEL = "/2/e/" private const val JSON_TOPIC_LEVEL = "/2/json/" private const val DEFAULT_SERVER_ADDRESS = "mqtt.meshtastic.org" - private const val KEEPALIVE_SECONDS = 30 private const val INITIAL_RECONNECT_DELAY_MS = 1000L private const val MAX_RECONNECT_DELAY_MS = 30_000L private const val RECONNECT_BACKOFF_MULTIPLIER = 2 } - @Volatile private var client: MqttClient? = null - - private val _connectionState = MutableStateFlow(ConnectionState.Disconnected.Idle) - override val connectionState: StateFlow = _connectionState.asStateFlow() + @Volatile private var client: MQTTClient? = null @OptIn(ExperimentalSerializationApi::class) private val json = Json { @@ -82,17 +77,25 @@ class MQTTRepositoryImpl( exceptionsWithDebugInfo = false } private val scope = CoroutineScope(dispatchers.default + SupervisorJob()) + + @Volatile private var clientJob: Job? = null private val publishSemaphore = Semaphore(20) + @Suppress("TooGenericExceptionCaught") override fun disconnect() { Logger.i { "MQTT Disconnecting" } val c = client - client = null - _connectionState.value = ConnectionState.Disconnected.Idle - scope.launch { safeCatching { c?.close() }.onFailure { e -> Logger.w(e) { "MQTT clean disconnect failed" } } } + client = null // Null first to prevent re-entrant disconnect + try { + c?.disconnect(ReasonCode.SUCCESS) + } catch (e: Exception) { + Logger.w(e) { "MQTT clean disconnect failed" } + } + clientJob?.cancel() + clientJob = null } - @OptIn(ExperimentalSerializationApi::class) + @OptIn(ExperimentalUnsignedTypes::class) override val proxyMessageFlow: Flow = callbackFlow { val ownerId = "MeshtasticAndroidMqttProxy-${nodeRepository.myId.value ?: "unknown"}" val channelSet = radioConfigRepository.channelSetFlow.first() @@ -100,105 +103,108 @@ class MQTTRepositoryImpl( val rootTopic = mqttConfig?.root?.ifEmpty { DEFAULT_TOPIC_ROOT } ?: DEFAULT_TOPIC_ROOT - val rawAddress = mqttConfig?.address ?: DEFAULT_SERVER_ADDRESS - val endpoint = resolveEndpoint(rawAddress, mqttConfig?.tls_enabled == true) + val (host, port) = + (mqttConfig?.address ?: DEFAULT_SERVER_ADDRESS).split(":", limit = 2).let { + it[0] to (it.getOrNull(1)?.toIntOrNull() ?: if (mqttConfig?.tls_enabled == true) 8883 else 1883) + } val newClient = - MqttClient(ownerId) { - keepAliveSeconds = KEEPALIVE_SECONDS - autoReconnect = true - username = mqttConfig?.username - mqttConfig?.password?.let { password(it) } - } + MQTTClient( + mqttVersion = MQTTVersion.MQTT5, + address = host, + port = port, + tls = if (mqttConfig?.tls_enabled == true) TLSClientSettings() else null, + userName = mqttConfig?.username, + password = mqttConfig?.password?.encodeToByteArray()?.toUByteArray(), + clientId = ownerId, + publishReceived = { packet -> + val topic = packet.topicName + val payload = packet.payload?.toByteArray() + Logger.d { "MQTT received message on topic $topic (size: ${payload?.size ?: 0} bytes)" } + + if (topic.contains("/json/")) { + try { + val jsonStr = payload?.decodeToString() ?: "" + // Validate JSON by parsing it + json.decodeFromString(jsonStr) + Logger.d { "MQTT parsed JSON payload successfully" } + + trySend(MqttClientProxyMessage(topic = topic, text = jsonStr, retained = packet.retain)) + } catch (e: JsonDecodingException) { + @OptIn(ExperimentalSerializationApi::class) + Logger.e(e) { "Failed to parse MQTT JSON: ${e.shortMessage} (path: ${e.path})" } + } catch (e: SerializationException) { + Logger.e(e) { "Failed to parse MQTT JSON: ${e.message}" } + } catch (e: IllegalArgumentException) { + Logger.e(e) { "Failed to parse MQTT JSON: ${e.message}" } + } + } else { + trySend( + MqttClientProxyMessage( + topic = topic, + data_ = payload?.toByteString() ?: okio.ByteString.EMPTY, + retained = packet.retain, + ), + ) + } + }, + ) + client = newClient - val subscriptions: List = buildList { - channelSet.subscribeList.forEach { globalId -> - add( - Subscription( - "$rootTopic$DEFAULT_TOPIC_LEVEL$globalId/+", - maxQos = QoS.AT_LEAST_ONCE, - noLocal = true, - ), + // Subscribe before starting the event loop. KMQTT's subscribe() calls send(), + // which queues the SUBSCRIBE packet in pendingSendMessages while connackReceived + // is false. Once the event loop receives CONNACK, it flushes the queue — so + // subscriptions are guaranteed to be sent immediately after the connection is + // established, with no timing races. This replaces a previous yield()-based + // approach that was unreliable on lightly loaded dispatchers. + val subscriptions = mutableListOf() + channelSet.subscribeList.forEach { globalId -> + subscriptions.add( + Subscription("$rootTopic$DEFAULT_TOPIC_LEVEL$globalId/+", SubscriptionOptions(Qos.AT_LEAST_ONCE)), + ) + if (mqttConfig?.json_enabled == true) { + subscriptions.add( + Subscription("$rootTopic$JSON_TOPIC_LEVEL$globalId/+", SubscriptionOptions(Qos.AT_LEAST_ONCE)), ) - if (mqttConfig?.json_enabled == true) { - add( - Subscription( - "$rootTopic$JSON_TOPIC_LEVEL$globalId/+", - maxQos = QoS.AT_LEAST_ONCE, - noLocal = true, - ), - ) - } - } - add(Subscription("$rootTopic${DEFAULT_TOPIC_LEVEL}PKI/+", maxQos = QoS.AT_LEAST_ONCE, noLocal = true)) - } - - // Collect from the SharedFlow before connecting to avoid missing retained messages - // that arrive immediately after SUBSCRIBE. - launch { newClient.messages.collect { msg -> processMessage(msg) } } - - // Forward the client's connection state to the repo-level StateFlow for UI observation. - launch { newClient.connectionState.collect { _connectionState.value = it } } - - // Retry the initial connect with exponential backoff. Once established, - // autoReconnect handles subsequent drops and re-subscribes internally. - launch { - var reconnectDelay = INITIAL_RECONNECT_DELAY_MS - while (true) { - val result = safeCatching { - Logger.i { "MQTT Connecting to $endpoint" } - newClient.connect(endpoint) - if (subscriptions.isNotEmpty()) { - Logger.d { "MQTT subscribing to ${subscriptions.size} topics" } - newClient.subscribe(subscriptions) - } - Logger.i { "MQTT connected and subscribed" } - } - when { - result.isSuccess -> return@launch - result.exceptionOrNull() is MqttException.ConnectionRejected -> { - Logger.e(result.exceptionOrNull()) { "MQTT connection rejected (unrecoverable), stopping" } - close(result.exceptionOrNull()!!) - return@launch - } - else -> { - Logger.e(result.exceptionOrNull()) { "MQTT connect failed, retrying in ${reconnectDelay}ms" } - delay(reconnectDelay) - reconnectDelay = - (reconnectDelay * RECONNECT_BACKOFF_MULTIPLIER).coerceAtMost(MAX_RECONNECT_DELAY_MS) - } - } } } + subscriptions.add(Subscription("$rootTopic${DEFAULT_TOPIC_LEVEL}PKI/+", SubscriptionOptions(Qos.AT_LEAST_ONCE))) + + if (subscriptions.isNotEmpty()) { + Logger.d { "MQTT subscribing to ${subscriptions.size} topics" } + newClient.subscribe(subscriptions) + } + + clientJob = + scope.launch { + var reconnectDelay = INITIAL_RECONNECT_DELAY_MS + while (true) { + try { + Logger.i { "MQTT Starting client loop for $host:$port" } + newClient.runSuspend() + // runSuspend returned normally — broker closed connection cleanly. + // Reset backoff so the next reconnect starts with the minimum delay. + reconnectDelay = INITIAL_RECONNECT_DELAY_MS + Logger.w { "MQTT client loop ended normally, reconnecting in ${reconnectDelay}ms" } + } catch (e: MQTTException) { + Logger.e(e) { "MQTT Client loop error (MQTT), reconnecting in ${reconnectDelay}ms" } + } catch (e: IOException) { + Logger.e(e) { "MQTT Client loop error (IO), reconnecting in ${reconnectDelay}ms" } + } catch (e: CancellationException) { + Logger.i { "MQTT Client loop cancelled" } + throw e + } + delay(reconnectDelay) + reconnectDelay = + (reconnectDelay * RECONNECT_BACKOFF_MULTIPLIER).coerceAtMost(MAX_RECONNECT_DELAY_MS) + } + } awaitClose { disconnect() } } - @OptIn(ExperimentalSerializationApi::class) - private fun ProducerScope.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(jsonStr) - Logger.d { "MQTT parsed JSON payload successfully" } - trySend(MqttClientProxyMessage(topic = topic, text = jsonStr, retained = msg.retain)) - } catch (e: JsonDecodingException) { - Logger.e(e) { "Failed to parse MQTT JSON: ${e.shortMessage} (path: ${e.path})" } - } catch (e: SerializationException) { - Logger.e(e) { "Failed to parse MQTT JSON: ${e.message}" } - } catch (e: IllegalArgumentException) { - Logger.e(e) { "Failed to parse MQTT JSON: ${e.message}" } - } - } else { - trySend(MqttClientProxyMessage(topic = topic, data_ = payload.toByteString(), retained = msg.retain)) - } - } - + @OptIn(ExperimentalUnsignedTypes::class) override fun publish(topic: String, data: ByteArray, retained: Boolean) { val currentClient = client if (currentClient == null) { @@ -208,36 +214,18 @@ class MQTTRepositoryImpl( Logger.d { "MQTT publishing message to topic $topic (size: ${data.size} bytes, retained: $retained)" } scope.launch { publishSemaphore.withPermit { - safeCatching { + @Suppress("TooGenericExceptionCaught") + try { currentClient.publish( - MqttMessage(topic = topic, payload = data, qos = QoS.AT_LEAST_ONCE, retain = retained), + retain = retained, + qos = Qos.AT_LEAST_ONCE, + topic = topic, + payload = data.toUByteArray(), ) + } catch (e: Exception) { + Logger.w(e) { "MQTT publish to $topic failed" } } - .onFailure { e -> Logger.w(e) { "MQTT publish to $topic failed" } } } } } } - -/** - * Resolve a user-supplied broker address into an [MqttEndpoint]. - * - * Address resolution rules: - * - If [rawAddress] already contains a URI scheme (`scheme://…`), parse it directly via [MqttEndpoint.parse] and - * respect whatever transport / port the user encoded. - * - Otherwise wrap it as a WebSocket endpoint (`ws[s]://host${WEBSOCKET_PATH}`) so the proxy works over CDNs and - * firewall-restricted networks where raw 1883/8883 may be blocked. The scheme is `wss` when [tlsEnabled] is `true`, - * `ws` otherwise. - * - * Extracted as a top-level function so [MQTTRepositoryImplTest] can exercise every branch without spinning up the full - * repository, and so `MqttManagerImpl` (in `:core:data`) can reuse the same parsing rules for the probe API. Visibility - * is `public` because Kotlin's `internal` is scoped per Gradle module. - */ -fun resolveEndpoint(rawAddress: String, tlsEnabled: Boolean): MqttEndpoint = if (rawAddress.contains("://")) { - MqttEndpoint.parse(rawAddress) -} else { - val scheme = if (tlsEnabled) "wss" else "ws" - MqttEndpoint.parse("$scheme://$rawAddress$WEBSOCKET_PATH") -} - -private const val WEBSOCKET_PATH = "/mqtt" diff --git a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/BleRadioTransportTest.kt b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/BleRadioTransportTest.kt index 840dc214a..f1049f897 100644 --- a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/BleRadioTransportTest.kt +++ b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/radio/BleRadioTransportTest.kt @@ -22,7 +22,6 @@ import dev.mokkery.every import dev.mokkery.matcher.any import dev.mokkery.mock import dev.mokkery.verify -import dev.mokkery.verify.VerifyMode import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy @@ -96,10 +95,10 @@ class BleRadioTransportTest { * [RadioInterfaceService.onDisconnect] must be called so the higher layers can react (e.g. start the device-sleep * timeout in [MeshConnectionManagerImpl]). * - * Virtual-time breakdown (DEFAULT_FAILURE_THRESHOLD = 3, DEFAULT_SETTLE_DELAY = 3 s): t = 3 000 ms — iteration 1 - * settle delay elapses, connectAndAwait throws, backoff 5 s starts t = 8 000 ms — backoff ends t = 11 000 ms — - * iteration 2 settle delay elapses, connectAndAwait throws, backoff 10 s starts t = 21 000 ms — backoff ends t = 24 - * 000 ms — iteration 3 settle delay elapses, connectAndAwait throws → onDisconnect called + * Virtual-time breakdown (DEFAULT_FAILURE_THRESHOLD = 3): t = 1 000 ms — iteration 1 settle delay elapses, + * connectAndAwait throws, backoff 5 s starts t = 6 000 ms — backoff ends t = 7 000 ms — iteration 2 settle delay + * elapses, connectAndAwait throws, backoff 10 s starts t = 17 000 ms — backoff ends t = 18 000 ms — iteration 3 + * settle delay elapses, connectAndAwait throws → onDisconnect called */ @Test fun `onDisconnect is called after DEFAULT_FAILURE_THRESHOLD consecutive failures`() = runTest { @@ -120,10 +119,10 @@ class BleRadioTransportTest { ) bleTransport.start() - // Advance through exactly 3 failure iterations (≈24 001 ms virtual time). + // Advance through exactly 3 failure iterations (≈18 001 ms virtual time). // The 4th iteration's backoff hasn't elapsed yet, so the coroutine is suspended // and advanceTimeBy returns cleanly. - advanceTimeBy(24_001L) + advanceTimeBy(18_001L) verify { service.onDisconnect(any(), any()) } @@ -132,17 +131,16 @@ class BleRadioTransportTest { } /** - * Reconnect policy must NEVER give up on its own. The transport is only ever instantiated for the user-selected - * device, and explicit-disconnect is owned by the service layer (close()). Even after a sustained failure storm — - * well beyond the legacy [BleReconnectPolicy.DEFAULT_MAX_FAILURES] — the transport must keep retrying and must - * never call `onDisconnect(isPermanent = true)` from the give-up path. + * After [BleReconnectPolicy.DEFAULT_MAX_FAILURES] (10) consecutive failures, the reconnect loop should stop and + * signal a permanent disconnect. This prevents infinite battery drain when the device is genuinely offline. * - * Time budget for 15 failures with bonded device (no scan): each iteration ≈ 3 s settle + immediate throw + - * backoff. Backoffs cap at 60 s after failure 5: 5+10+20+40+60+60+60+60+60+60+60+60+60+60+60 = 735 s, plus 15×3 s - * settle = 45 s, total ≈ 780 s. Use 800_000 ms to cover variance. + * Time budget for 10 failures with bonded device (no scan): Each iteration = 1s settle + connectAndAwait throw + + * backoff Backoffs: 5s, 10s, 20s, 40s, 60s, 60s, 60s, 60s, 60s, (exit at failure 10 before backoff) Total ≈ 10×1s + * settle + 5+10+20+40+60+60+60+60+60 = 10 + 375 = 385s ≈ 385_000ms We use a generous 400_000ms to cover any timing + * variance. */ @Test - fun `reconnect loop never gives up - no permanent disconnect from policy`() = runTest { + fun `reconnect loop stops after DEFAULT_MAX_FAILURES with permanent disconnect`() = runTest { val device = FakeBleDevice(address = address, name = "Test Device") bluetoothRepository.bond(device) @@ -160,13 +158,11 @@ class BleRadioTransportTest { ) bleTransport.start() - // Run well past where the legacy policy (maxFailures = 10) would have given up. - advanceTimeBy(800_001L) + // Advance enough time for all 10 failures to occur. + advanceTimeBy(400_001L) - // Transient disconnects (isPermanent = false) are expected once the failure threshold is hit; - // the policy must NEVER signal a permanent disconnect on its own. Only explicit close() - // (verified separately by the service layer) may emit isPermanent = true. - verify(mode = VerifyMode.not) { service.onDisconnect(isPermanent = true, errorMessage = any()) } + // Should have been called with isPermanent=true at least once (the final call). + verify { service.onDisconnect(isPermanent = true, errorMessage = any()) } bleTransport.close() } diff --git a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImplTest.kt b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImplTest.kt index 26b83a420..73e096da9 100644 --- a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImplTest.kt +++ b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImplTest.kt @@ -18,82 +18,25 @@ package org.meshtastic.core.network.repository import kotlinx.serialization.json.Json import org.meshtastic.core.model.MqttJsonPayload -import org.meshtastic.mqtt.MqttEndpoint import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertIs import kotlin.test.assertTrue class MQTTRepositoryImplTest { - // region resolveEndpoint — every behavioral branch of address parsing. - @Test - fun `bare host without scheme is wrapped as ws WebSocket on the standard port`() { - val endpoint = resolveEndpoint(rawAddress = "broker.example.com", tlsEnabled = false) + fun `test address parsing logic`() { + val address1 = "mqtt.example.com:1883" + val (host1, port1) = address1.split(":", limit = 2).let { it[0] to (it.getOrNull(1)?.toIntOrNull() ?: 1883) } + assertEquals("mqtt.example.com", host1) + assertEquals(1883, port1) - val ws = assertIs(endpoint) - assertEquals("ws://broker.example.com/mqtt", ws.url) + val address2 = "mqtt.example.com" + val (host2, port2) = address2.split(":", limit = 2).let { it[0] to (it.getOrNull(1)?.toIntOrNull() ?: 1883) } + assertEquals("mqtt.example.com", host2) + assertEquals(1883, port2) } - @Test - fun `bare host with TLS enabled is upgraded to wss`() { - val endpoint = resolveEndpoint(rawAddress = "broker.example.com", tlsEnabled = true) - - val ws = assertIs(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(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(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(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(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(endpoint) - assertEquals("broker.example.com", tcp.host) - assertEquals(8883, tcp.port) - assertEquals(true, tcp.tls) - } - - // endregion - - // region MqttJsonPayload — keep the existing JSON contract tests. - @Test fun `test json payload parsing`() { val jsonStr = @@ -129,6 +72,4 @@ class MQTTRepositoryImplTest { assertTrue(jsonStr.contains("\"from\":12345678")) assertTrue(jsonStr.contains("\"payload\":\"Hello World\"")) } - - // endregion } diff --git a/core/network/src/jvmAndroidMain/kotlin/org/meshtastic/core/network/radio/TcpRadioTransport.kt b/core/network/src/jvmAndroidMain/kotlin/org/meshtastic/core/network/radio/TcpRadioTransport.kt index 202d8de57..354c4cd30 100644 --- a/core/network/src/jvmAndroidMain/kotlin/org/meshtastic/core/network/radio/TcpRadioTransport.kt +++ b/core/network/src/jvmAndroidMain/kotlin/org/meshtastic/core/network/radio/TcpRadioTransport.kt @@ -78,11 +78,7 @@ open class TcpRadioTransport( Logger.d { "[$address] Closing TCP transport" } closing = true transport.stop() - // Do NOT emit onDisconnect(isPermanent = true) here. The explicit-disconnect signal is the - // service layer's responsibility (SharedRadioInterfaceService.stopTransportLocked); emitting - // it from close() caused a double-disconnect and prevented the auto-reconnect loop from - // owning its own lifecycle. The `closing` guard above suppresses the listener's transient - // disconnect during teardown. + callback.onDisconnect(isPermanent = true) } override fun keepAlive() { diff --git a/core/network/src/jvmMain/kotlin/org/meshtastic/core/network/SerialTransport.kt b/core/network/src/jvmMain/kotlin/org/meshtastic/core/network/SerialTransport.kt index 45ba70eb7..a3f34d67e 100644 --- a/core/network/src/jvmMain/kotlin/org/meshtastic/core/network/SerialTransport.kt +++ b/core/network/src/jvmMain/kotlin/org/meshtastic/core/network/SerialTransport.kt @@ -129,10 +129,7 @@ private constructor( // Ignore errors during port close } if (isActive) { - // Serial read loop ended unexpectedly (cable unplug, I/O error). Treat as - // transient — the user did not explicitly disconnect, and the port may come - // back when the device is replugged or the OS re-enumerates it. - onDeviceDisconnect(waitForStopped = true, isPermanent = false) + onDeviceDisconnect(true) } } } @@ -172,10 +169,8 @@ private constructor( private const val READ_TIMEOUT_MS = 100 /** - * Creates and opens a [SerialTransport]. If the port cannot be opened, the transport signals a transient - * disconnect to the [callback] and returns the (non-connected) instance. The open failure is treated as - * non-permanent so higher-layer reconnect orchestration can retry (e.g. when the device is replugged or the - * user grants permission); only an explicit close should signal a permanent disconnect. + * Creates and opens a [SerialTransport]. If the port cannot be opened, the transport signals a permanent + * disconnect to the [callback] and returns the (non-connected) instance. */ fun open( portName: String, @@ -188,7 +183,7 @@ private constructor( if (!transport.startConnection()) { val errorMessage = diagnoseOpenFailure(portName) Logger.w { "[$portName] Serial port could not be opened; signalling disconnect. $errorMessage" } - callback.onDisconnect(isPermanent = false, errorMessage = errorMessage) + callback.onDisconnect(isPermanent = true, errorMessage = errorMessage) } return transport } diff --git a/core/proto/consumer-rules.pro b/core/proto/consumer-rules.pro new file mode 100644 index 000000000..e9dc3751a --- /dev/null +++ b/core/proto/consumer-rules.pro @@ -0,0 +1,43 @@ +# Core proto classes required for packet handling and serialization +# FromRadio and related message types (primary packet container) +-keep class org.meshtastic.proto.FromRadio +-keep class org.meshtastic.proto.Data +-keep class org.meshtastic.proto.MeshPacket +-keep class org.meshtastic.proto.LogRecord + +# Message type payloads (handled in packet routing) +-keep class org.meshtastic.proto.AdminMessage +-keep class org.meshtastic.proto.StoreAndForward +-keep class org.meshtastic.proto.StoreForwardPlusPlus +-keep class org.meshtastic.proto.Routing + +# User and Node information +-keep class org.meshtastic.proto.User +-keep class org.meshtastic.proto.NeighborInfo +-keep class org.meshtastic.proto.Neighbor + +# Location and environment data +-keep class org.meshtastic.proto.Position +-keep class org.meshtastic.proto.Waypoint +-keep class org.meshtastic.proto.StatusMessage + +# Telemetry data types +-keep class org.meshtastic.proto.Telemetry +-keep class org.meshtastic.proto.DeviceMetrics +-keep class org.meshtastic.proto.EnvironmentMetrics +-keep class org.meshtastic.proto.AirQualityMetrics +-keep class org.meshtastic.proto.PowerMetrics +-keep class org.meshtastic.proto.LocalStats +-keep class org.meshtastic.proto.HostMetrics + +# Other data +-keep class org.meshtastic.proto.Paxcount +-keep class org.meshtastic.proto.DeviceMetadata + +# Configuration classes +-keep class org.meshtastic.proto.ChannelSet +-keep class org.meshtastic.proto.LocalConfig +-keep class org.meshtastic.proto.Config +-keep class org.meshtastic.proto.ModuleConfig +-keep class org.meshtastic.proto.Channel +-keep class org.meshtastic.proto.ClientNotification diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MqttManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MqttManager.kt index 6701514f8..7ebfa0521 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MqttManager.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MqttManager.kt @@ -16,16 +16,10 @@ */ package org.meshtastic.core.repository -import kotlinx.coroutines.flow.StateFlow -import org.meshtastic.core.model.MqttConnectionState -import org.meshtastic.core.model.MqttProbeStatus import org.meshtastic.proto.MqttClientProxyMessage /** Interface for managing MQTT proxy communication. */ interface MqttManager { - /** Observable MQTT proxy connection state for UI consumption. */ - val mqttConnectionState: StateFlow - /** Starts the MQTT proxy with the given settings. */ fun startProxy(enabled: Boolean, proxyToClientEnabled: Boolean) @@ -34,15 +28,4 @@ interface MqttManager { /** Handles an MQTT proxy message from the radio. */ fun handleMqttProxyMessage(message: MqttClientProxyMessage) - - /** - * Probe an MQTT broker to verify connectivity and credentials without joining the proxy lifecycle. Intended for UI - * "Test Connection" affordances. - * - * @param address Raw broker address as the user would type it (host, host:port, or full URL). - * @param tlsEnabled `true` to upgrade bare addresses to `wss://` (ignored when [address] already has a scheme). - * @param username Optional MQTT username. - * @param password Optional MQTT password. - */ - suspend fun probe(address: String, tlsEnabled: Boolean, username: String?, password: String?): MqttProbeStatus } diff --git a/core/resources/src/commonMain/composeResources/values-ar/strings.xml b/core/resources/src/commonMain/composeResources/values-ar/strings.xml index 2e4eaf53c..fc61e78d4 100644 --- a/core/resources/src/commonMain/composeResources/values-ar/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-ar/strings.xml @@ -154,7 +154,6 @@ الرسائل إعدادات لورا الجهة - انقطع الاتصال استغرق وقت طويل المسافة الإعدادات @@ -174,5 +173,4 @@ إعدادات بلوتوث - عربي diff --git a/core/resources/src/commonMain/composeResources/values-be/strings.xml b/core/resources/src/commonMain/composeResources/values-be/strings.xml index cb615de37..301ff3bb4 100644 --- a/core/resources/src/commonMain/composeResources/values-be/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-be/strings.xml @@ -167,8 +167,6 @@ Граць LoRa Рэгіён - Адлучана - Злучаны Імя карыстальніка Пароль Уключана @@ -222,5 +220,4 @@ Сіні Зялёны Meshtastic - Фільтраваць diff --git a/core/resources/src/commonMain/composeResources/values-bg/strings.xml b/core/resources/src/commonMain/composeResources/values-bg/strings.xml index f69e137d9..fe1520458 100644 --- a/core/resources/src/commonMain/composeResources/values-bg/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-bg/strings.xml @@ -201,15 +201,10 @@ Възстановяване на настройките по подразбиране Приложи Тема - Контраст Светла Тъмна По подразбиране на системата Избор на тема - Ниво на контраста - Стандартен - Среден - Висок Изпращане на местоположение в мрежата Компактно кодиране за Кирилица @@ -306,8 +301,6 @@ Батерия Използване на канала Използване на ефира - %1$s: %2$s%% - %1$s: %2$s V %1$s %1$s: %2$s записа @@ -324,9 +317,12 @@ Публичният ключ не съвпада със записания ключ. Можете да премахнете възела и да го оставите да обмени ключове отново, но това може да показва по-сериозен проблем със сигурността. Свържете се с потребителя чрез друг надежден канал, за да определите дали промяната на ключа се дължи на фабрично нулиране или друго умишлено действие. Известия за нови възли SNR + Съотношение сигнал/шум, мярка, използвана в комуникациите за количествено определяне на нивото на желания сигнал спрямо нивото на фоновия шум. В Meshtastic и други безжични системи, по-високото съотношение сигнал/шум показва по-ясен сигнал, който може да подобри надеждността и качеството на предаване на данни. RSSI + Индикатор за силата на получения сигнал - измерване, използвано за определяне на нивото на получения сигнал, приемано от антената. По-високата стойност на RSSI обикновено показва по-силна и по-стабилна връзка. (Качество на въздуха в помещенията) относителна скала за IAQ, стойностите са измерени с Bosch BME680. Диапазон на стойностите 0–500. Метрики на устройството + Карта на възела Позиция Последна актуализация на позицията Показатели на околната среда @@ -366,6 +362,7 @@ Макс Мин + Ср Разгъване на диаграмата Свиване на диаграмата Неизвестна възраст @@ -486,17 +483,6 @@ Честотен слот Игнориране на MQTT Конфигуриране на MQTT - Неактивен - Прекъсната връзка - Свързване… - Свързано - Повторно свързване… - Повторно свързване (опит %1$d) — %2$s - Тестване на връзката - Достъпен. Брокерът е приел идентификационните данни. - Достъпен (%1$s) - Хостът не е намерен - Връзката е неуспешна MQTT е активиран Адрес Потребителско име @@ -974,9 +960,5 @@ Въведете или изберете мрежа WiFi е конфигуриран успешно! Прилагането на конфигурацията за WiFi не е успешно - Изход Meshtastic - Филтър - Изберете устройство - Изберете мрежа diff --git a/core/resources/src/commonMain/composeResources/values-ca/strings.xml b/core/resources/src/commonMain/composeResources/values-ca/strings.xml index 22b52e28e..485e1c9e1 100644 --- a/core/resources/src/commonMain/composeResources/values-ca/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-ca/strings.xml @@ -182,7 +182,6 @@ Sempre Traçar ruta Regió - Desconnectat Temps esgotat Distància Meshtastic @@ -201,5 +200,4 @@ Meshtastic - Filtre diff --git a/core/resources/src/commonMain/composeResources/values-cs/strings.xml b/core/resources/src/commonMain/composeResources/values-cs/strings.xml index d3e0566ac..6220218ac 100644 --- a/core/resources/src/commonMain/composeResources/values-cs/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-cs/strings.xml @@ -331,9 +331,12 @@ Informace o uživateli Oznámení o nových uzlech SNR + 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. RSSI + 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í. (Vnitřní kvalita ovzduší) relativní hodnota IAQ měřená Bosch BME680. Hodnota rozsahu 0–500. Metriky zařízení + Mapa uzlu Pozice Poslední aktualizace pozice Metriky prostředí @@ -522,8 +525,6 @@ Ignorovat MQTT OK do MQTT Nastavení MQTT - Odpojeno - Připojeno MQTT povoleno Adresa Uživatelské jméno @@ -968,5 +969,4 @@ Připojit Hotovo Meshtastic - Filtr diff --git a/core/resources/src/commonMain/composeResources/values-de/strings.xml b/core/resources/src/commonMain/composeResources/values-de/strings.xml index 4755515ad..161feaa3e 100644 --- a/core/resources/src/commonMain/composeResources/values-de/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-de/strings.xml @@ -383,9 +383,12 @@ Benutzerinfo Benachrichtigung neue Knoten SNR + 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. RSSI + 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. (Innenluftqualität) relativer IAQ-Wert gemessen von Bosch BME680. Gerätedaten + Standortkarte Knoten Standort Letzte Standortaktualisierung Umweltdaten @@ -432,6 +435,7 @@ 1 Monat Maximal Minimum + Durchschnitt Diagramm einblenden Diagramm ausblenden Alter unbekannt @@ -609,23 +613,6 @@ MQTT ignorieren OK für MQTT MQTT Einstellungen - Inaktiv - Verbindung getrennt - Verbindung getrennt - %1$s - Wird verbunden - Verbunden - Erneut verbinden - Erneut verbinden (Versuch %1$d) - %2$s - Verbindung testen - Broker prüfen. - Erreichbar. Broker akzeptierte Anmeldedaten. - Erreichbar (%1$s) - Broker abgelehnt: %1$s - Host nicht gefunden - Broker (TCP) nicht erreichbar - TLS Handshake fehlgeschlagen - Zeitüberschreitung nach %1$d ms - Verbindung fehlgeschlagen MQTT aktiviert Adresse Benutzername @@ -1221,21 +1208,5 @@ Netzwerk eingeben oder auswählen WLAN erfolgreich konfiguriert! WLAN Konfiguration konnte nicht angewendet werden - Meshtastic Desktop - Meshtastic anzeigen - Beenden Meshtastic - TAK Datenpaket exportieren - Zeitzone löschen - Filter - Filter entfernen - Legende für Luftqualität anzeigen - Nachrichtenstatus anzeigen - Antwort senden - Nachricht kopieren - Nachricht auswählen - Nachricht löschen - Mit Emoji reagieren - Gerät auswählen - Wählen Sie ein Netzwerk diff --git a/core/resources/src/commonMain/composeResources/values-el/strings.xml b/core/resources/src/commonMain/composeResources/values-el/strings.xml index 8386ac2ea..88feab55e 100644 --- a/core/resources/src/commonMain/composeResources/values-el/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-el/strings.xml @@ -164,7 +164,6 @@ Μηνύματα LoRa Περιφέρεια - Αποσυνδεδεμένο Διεύθυνση Όνομα χρήστη Κωδικός πρόσβασης @@ -201,5 +200,4 @@ Κόκκινο Μπλε Πράσινο - Φίλτρο diff --git a/core/resources/src/commonMain/composeResources/values-es/strings.xml b/core/resources/src/commonMain/composeResources/values-es/strings.xml index 4c59aa547..3470f7bed 100644 --- a/core/resources/src/commonMain/composeResources/values-es/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-es/strings.xml @@ -300,10 +300,13 @@ Clave pública no coincide Notificaciones de nuevo nodo SNR + 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. RSSI + 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. (Calidad de Aire interior) escala relativa del valor IAQ como mediciones del sensor Bosch BME680. Rango de Valores 0 - 500. Métricas de Dispositivo + Mapa de Nodos Posición Última actualización Métricas de Entorno @@ -489,8 +492,6 @@ Rango de Valores 0 - 500. Ignorar Paquetes MQTT Permitir MQTT Configuración MQTT - Desconectado - Conectado Activar el MQTT Dirección del Servidor MQTT Usuario @@ -836,5 +837,4 @@ Estos datos de ubicación pueden ser utilizados para fines como aparecer en un m Conectar Hecho Meshtastic - Filtro diff --git a/core/resources/src/commonMain/composeResources/values-et/strings.xml b/core/resources/src/commonMain/composeResources/values-et/strings.xml index c2e327629..650c69122 100644 --- a/core/resources/src/commonMain/composeResources/values-et/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-et/strings.xml @@ -383,9 +383,12 @@ Kasutaja teave Uue sõlme teade SNR + 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. RSSI + 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. Siseõhu kvaliteet (IAQ) on suhtelise skaala väärtus, näiteks mõõtes Bosch BME680 abil. Väärtuste vahemik 0–500. Seadme mõõdikud + Sõlmede kaart Asukoht Viimase asukoha värskendus Keskkonnamõõdikud @@ -432,6 +435,7 @@ 1k Maksimaalselt Min + Keskm Laienda diagrammi Ahenda diagrammi Tundmatu vanus @@ -609,23 +613,6 @@ Keela MQTT Ok MQTTi MQTT sätted - Mitteaktiivne - Ühendus katkenud - Ühendus katkenud — %1$s - Ühendan… - Ühendatud - Taas ühendan… - Ühendan uuesti (katse %1$d) — %2$s - Test ühendus - Kontrollin vahendajat… - Ühendus õnnestus. Vahendaja aktsepteeris kasutajateave. - Kättesaadav (%1$s) - Vahendaja lükkas tagasi: %1$s - Hosti ei leitud - Vahendajaga ei saa ühendust (TCP) - TLS ühendus ebaõnnestus - Ajaline katkestus peale %1$d ms - Ühendus ebaõnnestus MQTT lubatud Aadress Kasutajatunnus @@ -1221,21 +1208,5 @@ Sisestage või valige võrk WiFi edukalt seadistatud! WiFi sätete rakendamine ebaõnnestus - Meshtastic töölaud - Näita Meshtastic - Sule Kärgvõrgustik - Ekspordi TAK andmepakett - Eemalda ajatsoon - Filtreeri - Eemalda filter - Näita õhukvaliteedi ajalugu - Kuva sõnumi olek - Saada vastus - Kopeeri sõnum - Vali sõnum - Kustuta sõnum - Vasta emotikoniga - Vali seade - Vali võrk diff --git a/core/resources/src/commonMain/composeResources/values-fi/strings.xml b/core/resources/src/commonMain/composeResources/values-fi/strings.xml index f9da71dea..3d9e28e91 100644 --- a/core/resources/src/commonMain/composeResources/values-fi/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-fi/strings.xml @@ -383,9 +383,12 @@ Käyttäjätiedot Uuden laitteen ilmoitukset SNR + 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. RSSI + 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. Sisäilman laatu (IAQ) on suhteellinen asteikko, jota voidaan mitata mm. Bosch BME680 anturilla ja sen arvoväli on 0–500. Laitteen mittausloki + Laitekartta Sijainti Viimeisin sijainnin päivitys Ympäristöarvot @@ -432,6 +435,7 @@ 1 kk Kaikki Minimi + Keskiarvo Laajenna kaavio Pienennä kaavio Tuntematon ikä @@ -609,23 +613,6 @@ Ohita MQTT MQTT päällä MQTT asetukset - Passiivinen - Ei yhdistetty - Yhteys katkaistu — %1$s - Yhdistetään… - Yhdistetty - Yhdistetään uudelleen… - Yhdistetään uudelleen (yritys %1$d) — %2$s - Testaa yhteys - Tarkistetaan välityspalvelinta… - Yhteys onnistui. Välityspalvelin hyväksyi tunnistetiedot. - Yhteys onnistui (%1$s) - Välityspalvelin ei hyväksynyt: %1$s - Palvelinta ei löytynyt - Yhteyttä välityspalvelimeen ei saada (TCP) - TLS-yhteyden muodostus epäonnistui - Aikakatkaistu %1$d ms jälkeen - Yhdistäminen epäonnistui MQTT käytössä Osoite Käyttäjänimi @@ -1222,21 +1209,5 @@ Syötä tai valitse verkko WiFi määritetty onnistuneesti! WiFi-asetusten käyttöönotto epäonnistui - Meshtastic työpöytä - Näytä Meshtastic - Lopeta Meshtastic - Vie TAK-datapaketti - Tyhjennä aikavyöhyke - Suodatus - Poista suodatin - Näytä ilmanlaadun selite - Näytä viestin tila - Lähetä vastaus - Kopioi viesti - Valitse viesti - Poista viesti - Reaktio emojin kanssa - Valitse laite - Valitse verkko diff --git a/core/resources/src/commonMain/composeResources/values-fr/strings.xml b/core/resources/src/commonMain/composeResources/values-fr/strings.xml index f4afeef5c..0ef821cb6 100644 --- a/core/resources/src/commonMain/composeResources/values-fr/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-fr/strings.xml @@ -18,7 +18,6 @@ Meshtastic - Meshtastic %1$s Filtre Effacer le filtre de nœud Filtrer par @@ -41,11 +40,9 @@ Interne par Favoris Afficher uniquement les nœuds ignorés - Exclure MQTT Non reconnu En attente d'accusé de réception En file d'attente pour l'envoi - Délivré au nœud Inconnu Routage via chaîne SF++… Confirmé via chaîne SF++ @@ -122,8 +119,7 @@ Distance minimale en mètres pour considérer une diffusion de position intelligente. À quelle fréquence devrions-nous essayer d'obtenir une position GPS (<10sec le GPS est maintenu allumé). 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. - 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. - 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. + 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. Utilisée pour créer une clé partagée avec un appareil distant. Clé publique autorisée à envoyer des messages d’administration à ce nœud. L'appareil est géré par un administrateur de maillage, l'utilisateur ne peut accéder à aucun des paramètres de l'appareil. @@ -167,25 +163,18 @@ Port : Connecté Connexions actuelles : - IP du Wifi : + IP WiFi : IP Ethernet : Connexion en cours Non connecté Aucun appareil sélectionné Périphérique inconnu - Aucun périphérique réseau trouvé - Pas de périphérique USB trouvé - USB - Mode Démo Connecté à la radio, mais en mode veille Mise à jour de l’application requise 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 docs sur ce sujet. Aucun (désactivé) Notifications de service Remerciements - Bibliothèques Open Source - Meshtastic est construit avec les bibliothèques open source suivantes. Appuyez sur n'importe quelle bibliothèque pour voir sa licence. - %1$d Bibliothèques Cette URL de canal est invalide et ne peut pas être utilisée Panneau de débogage Contenu décodé : @@ -218,21 +207,7 @@ Correspondre à tout | N'importe quel 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. Effacer - Rechercher des émojis... - Plus d'actions Canal - %1$s: %2$s - Message de %1$s: %2$s - Entête - Élément %1$d - Pied de page - Exporter le paquet de données TAK - Point - Texte - Jauge - Dégradé - Ceci est un composable personnalisé - Avec plusieurs lignes et styles Statut d'envoi du message Nouveaux messages au-dessous Notifications de message @@ -253,15 +228,10 @@ Rétablir les valeurs par défaut Appliquer Thème - Contraste Clair Sombre Valeur par défaut du système Choisir un thème - Niveau de contraste - Standard - Milieu - Haut Fournir l'emplacement au maillage Encodage compact pour Cyrillique @@ -305,7 +275,6 @@ Message direct Reconfiguration de NodeDB Réception confirmée par le destinataire - Votre appareil peut se déconnecter et redémarrer lorsque les paramètres sont appliqués. Erreur Une erreur inconnue s'est produite Ignorer @@ -348,8 +317,6 @@ Actuellement : Toujours muet Non muet - Muet pour %1$d jours, %2$s heures - Muet pour %1$s heures Désactiver les notifications pour '%1$s' ? Réactiver les notifications pour '%1$s' ? Remplacer @@ -359,10 +326,7 @@ Batterie UtilCanal UtilAir - %1$s / %2$s%% - %1$s: %2$s V %1$s - %1$s: %2$s Temp Hum Temp sol @@ -383,9 +347,12 @@ Infos utilisateur Notifikasyon nouvo nœud SNR + 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. RSSI + 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. (Qualité de l'air intérieur) valeur de l'échelle relative IAQ mesurée par Bosch BME680. Plage de valeur 0–500. Métriques de l’appareil + Carte historique des positions Position Dernière mise à jour de position Métriques d'environnement @@ -414,26 +381,13 @@ Durée : %1$s s Route aller :\n\n Route retour :\n\n - Saut vers l'avant - Saut vers l'arrière - Aller/Retour Pas de réponse - Charge 1 m - Charge 5m - Charge 15 m - Moyenne de charge du système d'une minute - Moyenne de charge du système de cinq minutes - Moyenne de charge du système de 15 minutes - Mémoire système disponible en octets 1H 24H 1S 2S 1M Max - Min - Agrandir le graphique - Réduire le graphique Age inconnu Copier Caractère d'appel ! @@ -447,17 +401,11 @@ Canal 1 Canal 2 Canal 3 - Canal 4 - Canal 5 - Canal 6 - Canal 7 - Canal 8 Actif Tension Êtes-vous sûr ? Documentation du rôle de l'appareil et le billet de blog sur comment Choisir le rôle de l'appareil approprié.]]> Je sais ce que je fais. - La batterie du nœud %1$s est faible (%2$d%) Notifications de batterie faible Batterie faible : %1$s Notifications de batterie faible (nœuds favoris) @@ -583,9 +531,6 @@ Durée de sortie (en millisecondes) Durée de répétition de la sortie (secondes) Sonnerie - Sonnerie importée - Le fichier est vide - Erreur d'importation : %1$s Lancer Utiliser l'I2S comme buzzer LoRa @@ -609,13 +554,6 @@ Ignorer MQTT Transmission des paquets vers MQTT Configuration MQTT - Inactif - Déconnecté - Connexion… - Connecté - Reconnexion… - Test de la connexion - Échec de la connexion MQTT activé Adresse Nom d'utilisateur @@ -687,8 +625,6 @@ Série activée Écho activé Vitesse de transmission série - RX - Tx Délai d'expiration Mode série Outrepasser le port série de la console @@ -723,15 +659,8 @@ Distance Lux Vent - Vitesse du vent - Rafales de vent - Vent à la traîne - Direction du vent - Pluie (1h) - Pluie (24h) Poids Radiation - Températeur 1-Wire Qualité de l'air intérieur (IAQ) URL @@ -748,7 +677,6 @@ Horodatage En-tête Vitesse - %1$d Km/h Sats Alt Fréq @@ -814,11 +742,6 @@ Afficher les points de repère Afficher les cercles de précision Notification client - Vérification de la clé - Requête de vérification de clé - Vérification de la clé terminée - Clé publique dupliquée détectée - Clé de chiffrement faible détectée Clés compromises détectées, sélectionnez OK pour régénérer. Régénérer la clé privée Ê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. @@ -871,14 +794,7 @@ Composer un message Métriques de PAX PAX - PAX : %1$d - B:%1$d - W :%1$d - PAX : %1$s - BLE: %1$s - Wi-Fi : %1$s Aucune métrique PAX disponible. - Approvisionnement Wi-Fi pour mPWRD-OS Appareils Bluetooth Périphérique connecté Limite de débit dépassée. Veuillez réessayer plus tard. @@ -933,8 +849,6 @@ Terrain Hybride Gérer les calques de la carte - Les calques personnalisés prennent en charge les fichiers .kml, .kmz ou GeoJSON. - Aucun calque personnalisé chargé. Ajouter un calque Afficher le calque Supprimer le calque @@ -942,10 +856,6 @@ Nœuds à cet emplacement Type de carte sélectionné Gérer les sources de tuiles personnalisées - Ajouter un réseau de tuile personnalisée - Aucune source de tuiles personnalisées trouvée. - Modifier le réseau de tuile personnalisée - Supprimer le réseau de tuile personnalisée Le nom ne peut pas être vide. Le nom du fournisseur existe déjà. URL ne peut être vide. @@ -1039,7 +949,6 @@ Notes de Version Une erreur inconnue s'est produite Les informations de l'utilisateur du nœud sont manquantes. - Batterie trop faible (%1$d%). Veuillez charger votre appareil avant de mettre à jour. Impossible de récupérer le fichier firmware. Échec de la mise à jour USB Intégrité (hash) du firmware rejetée. Veuillez réessayer ou mettre à jours l'appareil via USB. @@ -1116,10 +1025,8 @@ Configuration Gérer à distance sans fil les paramètres et les canaux de votre appareil. Sélection du style de carte - Batterie : %1$d% Nœuds : %1$d en ligne / %2$d au total Temps de disponibilité : %1$s - ChUtil: %1$s% | AirTX: %2$s% Trafic : TX %1$d / RX %2$d (D: %3$d) Relais : %1$d (annulé: %2$d) Diagnostiques : %1$s @@ -1133,94 +1040,11 @@ Actualiser Mis à jour - Ajouter une couche de réseau - Fichier local MBTiles - Ajouter un fichier local MBTiles - TAK (ATAK) - Configuration TAK - Activer le serveur TAK local - Démarre un serveur TCP sur le port 8089 pour les connexions ATAK - Couleur de l'équipe - Rôle Membre - Non spécifié - Blanc - Jaune - Orange - Magenta Rouge - Marron - Pourpre - Bleu foncé Bleu - Cyan - Turquoise Vert - Vert Foncé - Marron - Non spécifié - Membre de l'équipe - Chef d'équipe - Quartier général - Tireur d'élite - Medic - Observateur de transfert - Opérateur de radio téléphonie - Doggo (K9) - Gestion du trafic - Configuration de la gestion du trafic Module activé - Déduplication de Position - Précision de position (octets) - Intervalle de position min (secs) - Réponse directe de NodeInfo - Max de saut pour une réponse directe - Limitation de débit - Fenêtre de limitation de taux (secs) - Paquets maximum dans la fenêtre - Ignorer les paquets inconnus - Seuil de paquets inconnu - Télémétrie locale uniquement (Relays) - Position locale uniquement (Relays) - Conserver les sauts du Routeur - Note - Stockage de l'appareil & UI (lecture seule) - Thème %1$s, Langue %2$s - Fichiers disponibles (%1$d ) : - - %1$s (%2$d octets) - Aucun fichier affiché. Connecter Terminé - Approvisionnement Wi-Fi pour mPWRD-OS - Fournissez les identifiants Wi-Fi à votre appareil mPWRD-OS via Bluetooth. - En savoir plus sur le projet mPWRD-OS\nhttps://github.com/mPWRD-OS - Recherche de l'appareil - Appareil détecté - Prêt à rechercher des réseaux WiFi. - Rechercher des réseaux - Recherche… - Application de la configuration WiFi… - Aucun réseau trouvé - Impossible de se connecter : %1$s - Échec de la recherche des réseaux WiFi : %1$s - %1$d% - Réseaux disponibles - Nom du réseau (SSID) - Saisir ou sélectionnez un réseau - WiFi configuré avec succès ! - Impossible d'appliquer la configuration WiFi - Meshtastic application de bureau - Afficher Meshtastic - Quitter Meshtastic - Exporter le paquet de données TAK - Filtre - Supprimer le filtre - Afficher le statut du message - Envoyer une réponse - Copier le message - Sélectionner le message - Supprimer le message - Réagir avec un emoji - Sélectionner l'appareil - Sélectionner le réseau diff --git a/core/resources/src/commonMain/composeResources/values-ga/strings.xml b/core/resources/src/commonMain/composeResources/values-ga/strings.xml index baabf41d0..a081daff2 100644 --- a/core/resources/src/commonMain/composeResources/values-ga/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-ga/strings.xml @@ -190,7 +190,10 @@ Cóid Poiblí Eochair Mícomhoiriúnacht na heochrach phoiblí Fógartha faoi na nodes nua + 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í. + 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. (Cáilíocht Aeir Inmheánach) scála ábhartha den luach QAÍ a thomhas ag Bosch BME680. Scála Luach 0–500. + Léarscáil an Node Rialachas Rialú iargúlta Go dona @@ -210,7 +213,6 @@ Céimeanna i dtreo %1$d Céimeanna ar ais %2$d Réigiún - Na ceangailte Am tráth Sáth @@ -227,5 +229,4 @@ - Scagaire diff --git a/core/resources/src/commonMain/composeResources/values-gl/strings.xml b/core/resources/src/commonMain/composeResources/values-gl/strings.xml index dc751d2e9..cc3c02597 100644 --- a/core/resources/src/commonMain/composeResources/values-gl/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-gl/strings.xml @@ -149,7 +149,6 @@ Sempre Traza-ruta Rexión - Desconectado Distancia @@ -165,5 +164,4 @@ - Filtro diff --git a/core/resources/src/commonMain/composeResources/values-he/strings.xml b/core/resources/src/commonMain/composeResources/values-he/strings.xml index 502d64056..3afe39071 100644 --- a/core/resources/src/commonMain/composeResources/values-he/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-he/strings.xml @@ -133,7 +133,6 @@ בדיקת מסלול הודעות אזור - מנותק מרחק הגדרות @@ -148,5 +147,4 @@ - פילטר diff --git a/core/resources/src/commonMain/composeResources/values-hr/strings.xml b/core/resources/src/commonMain/composeResources/values-hr/strings.xml index 114c3ed9a..aae7d6690 100644 --- a/core/resources/src/commonMain/composeResources/values-hr/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-hr/strings.xml @@ -150,7 +150,6 @@ Detalji Crveno Regija - Odspojeno Udaljenost Meshtastic @@ -169,5 +168,4 @@ Crveno Meshtastic - Filtriraj diff --git a/core/resources/src/commonMain/composeResources/values-ht/strings.xml b/core/resources/src/commonMain/composeResources/values-ht/strings.xml index 60e00d491..7c4fc0f24 100644 --- a/core/resources/src/commonMain/composeResources/values-ht/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-ht/strings.xml @@ -186,7 +186,10 @@ Chifreman Kle Piblik Pa matche kle piblik Notifikasyon nouvo nœud + 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. + 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. (Kalite Lèy Entèryè) echèl relatif valè IAQ jan li mezire pa Bosch BME680. Ranje valè 0–500. + Kat Nœud Administrasyon Administrasyon Remote Move @@ -198,7 +201,6 @@ Direk Hops vèsus %1$d Hops tounen %2$d Rejyon - Dekonekte Tan pase Distans @@ -215,5 +217,4 @@ - Filtre diff --git a/core/resources/src/commonMain/composeResources/values-hu/strings.xml b/core/resources/src/commonMain/composeResources/values-hu/strings.xml index 33b795a7f..f553a6a32 100644 --- a/core/resources/src/commonMain/composeResources/values-hu/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-hu/strings.xml @@ -316,9 +316,12 @@ Publikus kulcs nem egyezik Új állomás értesítések SNR + Jel–zaj 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. RSSI + 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. (Beltéri levegőminőség) relatív IAQ érték a Bosch BME680 szenzor alapján. Értéktartomány: 0–500. Eszközmetrikák + Állomás Térkép Pozíció Utolsó pozíciófrissítés Környezeti metrikák @@ -512,8 +515,6 @@ MQTT figyelmen kívül hagyása MQTT-re továbbítható MQTT beállítások - Szétkapcsolva - Csatlakoztatva MQTT engedélyezve Cím Felhasználónév @@ -850,5 +851,4 @@ Zöld Csatlakozás Meshtastic - Filter diff --git a/core/resources/src/commonMain/composeResources/values-is/strings.xml b/core/resources/src/commonMain/composeResources/values-is/strings.xml index ce8853250..4e07e1c2a 100644 --- a/core/resources/src/commonMain/composeResources/values-is/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-is/strings.xml @@ -119,7 +119,6 @@ Hámarsksendingartíma náð. Ekki hægt að senda skilaboð, vinsamlegast reynið aftur síðar. Ferilkönnun Svæði - Aftengd diff --git a/core/resources/src/commonMain/composeResources/values-it/strings.xml b/core/resources/src/commonMain/composeResources/values-it/strings.xml index baa0e0947..744741047 100644 --- a/core/resources/src/commonMain/composeResources/values-it/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-it/strings.xml @@ -355,9 +355,12 @@ Informazioni Utente Notifiche di nuovi nodi SNR + 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. RSSI + 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. (Qualità dell'aria interna) scala relativa del valore della qualità dell'aria indoor, misurato da Bosch BME680. Valore Intervallo 0–500. Metriche Dispositivo + Mappa Dei Nodi Posizione Aggiornamento ultima posizione Metriche Ambientali @@ -557,8 +560,6 @@ Ignora MQTT OK per MQTT Configurazione MQTT - Disconnesso - Connesso MQTT abilitato Indirizzo Username @@ -960,5 +961,4 @@ Connetti Fatto Meshtastic - Filtro diff --git a/core/resources/src/commonMain/composeResources/values-ja/strings.xml b/core/resources/src/commonMain/composeResources/values-ja/strings.xml index 64aa0fe05..59b54d2f5 100644 --- a/core/resources/src/commonMain/composeResources/values-ja/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-ja/strings.xml @@ -275,8 +275,11 @@ 公開キーが一致しません 新しいノードの通知 SN比 + 信号対ノイズ比(SN比)は、通信において、目的の信号のレベルを背景ノイズのレベルに対して定量化するために使用される尺度です。Meshtasticや他の無線システムでは、SN比が高いほど信号が鮮明であることを示し、データ伝送の信頼性と品質を向上させることができます。 RSSI + 受信信号強度インジケーター(RSSI)は、アンテナで受信している電力レベルを測定するための指標です。一般的にRSSI値が高いほど、より強力で安定した接続を示します。 (屋内空気品質) 相対スケールIAQ値は、ボッシュBME680によって測定されます。 値の範囲は 0-500。 + ノードマップ 位置 管理 リモート管理 @@ -421,8 +424,6 @@ PAファン無効 MQTT を無視 MQTT設定 - 切断 - 接続済 MQTTを有効化 アドレス ユーザー名 @@ -652,5 +653,4 @@ モジュール有効 接続 Meshtastic - 絞り込み diff --git a/core/resources/src/commonMain/composeResources/values-ko/strings.xml b/core/resources/src/commonMain/composeResources/values-ko/strings.xml index 914446a60..0a5bc4031 100644 --- a/core/resources/src/commonMain/composeResources/values-ko/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-ko/strings.xml @@ -213,8 +213,11 @@ 공개 키가 일치하지 않습니다 새로운 노드 알림 SNR + 통신에서 원하는 신호의 수준을 배경 잡음의 수준과 비교하여 정량화하는 데 사용되는 신호 대 잡음비 Signal-to-Noise Ratio, SNR는 Meshtastic와 같은 무선 시스템에서 SNR이 높을수록 더 선명한 신호를 나타내어 데이터 전송의 안정성과 품질을 향상시킬 수 있습니다. RSSI + 수신 신호 강도 지표 Received Signal Strength Indicator, RSSI는 안테나가 수신하는 신호의 전력 수준을 측정하는 데 사용되는 지표입니다. RSSI 값이 높을수록 일반적으로 더 강력하고 안정적인 연결을 나타냅니다. (실내공기질) Bosch BME680으로 측정한 상대적 척도 IAQ 값. 범위 0–500. + 노드 지도 위치 최근 위치 업데이트 관리 @@ -370,8 +373,6 @@ PA fan 비활성화됨 MQTT로 부터 수신 무시 MQTT 설정 - 연결 끊김 - 연결됨 MQTT 활성화 서버 주소 사용자명 @@ -539,5 +540,4 @@ 초록 연결 Meshtastic - 필터 diff --git a/core/resources/src/commonMain/composeResources/values-lt/strings.xml b/core/resources/src/commonMain/composeResources/values-lt/strings.xml index 33f5e4d59..9592d8b14 100644 --- a/core/resources/src/commonMain/composeResources/values-lt/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-lt/strings.xml @@ -190,6 +190,7 @@ Naujo įtaiso pranešimas SNR RSSI + Įtaisų žemėlapis Administravimas Nuotolinis administravimas Silpnas @@ -216,7 +217,6 @@ Skambučio simbolis! Raudona Regionas - Atsijungta Viešasis raktas Privatus raktas Baigėsi laikas @@ -237,5 +237,4 @@ Raudona - Filtras diff --git a/core/resources/src/commonMain/composeResources/values-nl/strings.xml b/core/resources/src/commonMain/composeResources/values-nl/strings.xml index b6972b6ec..ee07fb52b 100644 --- a/core/resources/src/commonMain/composeResources/values-nl/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-nl/strings.xml @@ -201,8 +201,11 @@ Publieke sleutel komt niet overeen Nieuwe node meldingen SNR + 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. RSSI + 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. (Binnenluchtkwaliteit) relatieve schaal IAQ waarde gemeten door Bosch BME680. Waarde tussen 0 en 500. + Node Kaart Positie Beheer Extern beheer @@ -309,8 +312,6 @@ Inkomende negeren Negeer MQTT MQTT Configuratie - Niet verbonden - Verbonden MQTT ingeschakeld Adres Gebruikersnaam @@ -415,5 +416,4 @@ Blauw Groen Verbinding maken - Filter diff --git a/core/resources/src/commonMain/composeResources/values-no/strings.xml b/core/resources/src/commonMain/composeResources/values-no/strings.xml index cd00c43e2..2ecd2a425 100644 --- a/core/resources/src/commonMain/composeResources/values-no/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-no/strings.xml @@ -194,8 +194,11 @@ Direktemeldinger bruker den nye offentlige nøkkelinfrastrukturen for kryptering. Krever firmware versjon 2.5 eller høyere. Varsel om nye noder SNR + 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. RSSI + \"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. (Innendørs luftkvalitet) relativ skala IAQ-verdi målt ved Bosch BME680. Verdi 0–500. + Nodekart Administrasjon Fjernadministrasjon Dårlig @@ -219,7 +222,6 @@ Kopier Varsel, bjellekarakter! Region - Frakoblet Offentlig nøkkel Privat nøkkel Tidsavbrudd @@ -238,5 +240,4 @@ - Filter diff --git a/core/resources/src/commonMain/composeResources/values-pl/strings.xml b/core/resources/src/commonMain/composeResources/values-pl/strings.xml index 7c9b3433b..272f515f8 100644 --- a/core/resources/src/commonMain/composeResources/values-pl/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-pl/strings.xml @@ -333,9 +333,12 @@ Informacje o użytkowniku Powiadomienia o nowych węzłach SNR: + 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. RSSI: + 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. Jakość powietrza w pomieszczeniach (Indoor Air Quality) - wartość względna w skali IAQ mierzona czujnikiem BME680. Zakres wartości: 0–500. Metryka urządzenia + Ślad na mapie Pozycjonowanie Ostatnia aktualizacja lokalizacji Metryki środowiskowe @@ -496,8 +499,6 @@ Zignoruj MQTT Ok dla MQTT Konfiguracja MQTT - Rozłączono - Połączony Włącz MQTT Adres Nazwa użytkownika @@ -749,5 +750,4 @@ Połącz Wykonano Meshtastic - Filtr diff --git a/core/resources/src/commonMain/composeResources/values-pt-rBR/strings.xml b/core/resources/src/commonMain/composeResources/values-pt-rBR/strings.xml index ac97b091c..521a84b48 100644 --- a/core/resources/src/commonMain/composeResources/values-pt-rBR/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-pt-rBR/strings.xml @@ -230,8 +230,11 @@ Chave pública não confere Novas notificações de nó SNR + 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. RSSI + 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. (Qualidade do ar interior) valor relativo da escala IAQ medido pelo Bosch BME680. Intervalo de Valor de 0–500. + Mapa do nó Posição Atualização da última posição Administração @@ -379,8 +382,6 @@ Ventilador do PA desativado Ignorar MQTT Configurações MQTT - Desconectado - Conectado MQTT habilitado Endereço Nome de usuário @@ -665,5 +666,4 @@ Verde Concluído Meshtastic - Filtro diff --git a/core/resources/src/commonMain/composeResources/values-pt/strings.xml b/core/resources/src/commonMain/composeResources/values-pt/strings.xml index a00bce554..545bd4e6f 100644 --- a/core/resources/src/commonMain/composeResources/values-pt/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-pt/strings.xml @@ -219,8 +219,11 @@ Incompatibilidade de chave pública Notificações de novos nodes SNR + 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. RSSI + 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. (Qualidade do ar interior) valor relativo da escala IAQ conforme medida por Bosch BME680. Entre 0–500. + Mapa de nodes Posição Administração Administração Remota @@ -363,8 +366,6 @@ Ignorar entrada Ignorar MQTT Configuração MQTT - Desconectado - Ligado MQTT ativo Endereço Utilizador @@ -515,5 +516,4 @@ Verde Ligar Nome do nó de alternativo - Filtrar diff --git a/core/resources/src/commonMain/composeResources/values-ro/strings.xml b/core/resources/src/commonMain/composeResources/values-ro/strings.xml index f9787ba93..984a939d8 100644 --- a/core/resources/src/commonMain/composeResources/values-ro/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-ro/strings.xml @@ -376,9 +376,12 @@ Info utilizator Notificări noduri noi SNR + 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. RSSI + 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ă. (Calitatea aerului interior) valoarea IAQ pe o scară relativă, măsurată cu Bosch BME680. Intervalul valorilor: 0–500. Valori dispozitiv + Harta nodurilor Poziție Ultima actualizare a poziției Indicatori de mediu @@ -421,6 +424,7 @@ 1W 2W Maxim + Medie Extindeți graficul Restrânge graficul Vârstă necunoscută @@ -595,8 +599,6 @@ Ignoră MQTT Acceptă MQTT Configurare MQTT - Deconectat - Conectat MQTT activat Adresă Nume de utilizator @@ -1167,5 +1169,4 @@ WiFi configurat cu succes! Nu s-a reușit aplicarea configurației Wi-Fi Meshtastic - Filtru diff --git a/core/resources/src/commonMain/composeResources/values-ru/strings.xml b/core/resources/src/commonMain/composeResources/values-ru/strings.xml index 8d4590e82..dd0d4a53f 100644 --- a/core/resources/src/commonMain/composeResources/values-ru/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-ru/strings.xml @@ -389,9 +389,12 @@ Пользовательская информация Уведомления о новых нодах Сигнал/шум + Соотношение сигнал/шум, мера, используемая в коммуникациях для количественной оценки уровня желаемого сигнала по отношению к уровню фонового шума. В Meshtastic и других беспроводных системах более высокий SNR указывает на более четкий сигнал, который может повысить надежность и качество передачи данных. RSSI + Индикатор уровня принимаемого сигнала, измерение, используемое для определения уровня мощности, принимаемой антенной. Более высокое значение RSSI обычно указывает на более сильное и стабильное соединение. (Качество воздуха в помещении) Относительная шкала IAQ, измеренная Bosch BME680. Диапазон значений 0–500. Интервал передачи + Карта нод Местоположение Обновление последнего местоположения Метрики окружения @@ -440,6 +443,7 @@ Макс Мин + Сред Развернуть диаграмму Свернуть диаграмму Неизвестный возраст @@ -617,23 +621,6 @@ Игнорировать MQTT ОК в MQTT Настройка MQTT - Неактивно - Отключено - Отключено — %1$s - Подключение... - Подключено - Переподключение... - Переподключение (попытка %1$d) — %2$s - Проверить соединение - Проверяем брокер… - Доступно. Брокер принял учетные данные. - Доступно (%1$s) - Брокер отклонен: %1$s - Узел не найден - Не удается подключиться к брокеру (TCP) - Сбой TLS-рукопожатия - Тайм-аут после %1$d мс - Соединение не удалось MQTT включен Адрес Имя пользователя @@ -1237,21 +1224,5 @@ Введите или выберите сеть Wi-Fi успешно настроен! Не удалось применить настройку Wi-Fi - Meshtastic Desktop - Показать Meshtastic - Выход Meshtastic - Экспорт пакета данных TAK - Очистить часовой пояс - Фильтр - Удалить фильтр - Показать легенду качества воздуха - Показать статус сообщения - Отправить ответ - Скопировать сообщение - Выбрать сообщение - Удалить сообщение - Отреагировать эмодзи - Выберите устройство - Выбрать сеть diff --git a/core/resources/src/commonMain/composeResources/values-sk/strings.xml b/core/resources/src/commonMain/composeResources/values-sk/strings.xml index 6beec1a74..e51ef506d 100644 --- a/core/resources/src/commonMain/composeResources/values-sk/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-sk/strings.xml @@ -257,8 +257,11 @@ Nezhoda verejného kľúča Notifikácie nových uzlov SNR + 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. RSSI + 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. (Kvalita vzduchu v interiéri) relatívna hodnota IAQ meraná prístrojom Bosch BME680. Rozsah hodnôt 0–500. + Mapa uzlov Pozícia Administrácia Administrácia na diaľku @@ -359,8 +362,6 @@ LoRa Šírka pásma Región - Odpojené - Pripojený Adresa Používateľské meno Heslo @@ -427,5 +428,4 @@ Modrá Zelená Meshtastic - Filter diff --git a/core/resources/src/commonMain/composeResources/values-sl/strings.xml b/core/resources/src/commonMain/composeResources/values-sl/strings.xml index bff8e6150..8025c4751 100644 --- a/core/resources/src/commonMain/composeResources/values-sl/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-sl/strings.xml @@ -196,8 +196,11 @@ Neujemanje javnega ključa Obvestila novih vozlišč SNR + 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. RSSI + 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. (Kakovost zraka v zaprtih prostorih) relativna vrednost IAQ na lestvici, izmerjena z Bosch BME680. Razpon vrednosti 0–500. + Zemljevid vozlišč Administracija Administracija na daljavo Slab @@ -223,7 +226,6 @@ Kopiraj Znak opozorilnega zvonca! Regija - Prekinjeno Javni ključ Zasebni ključ Časovna omejitev @@ -242,5 +244,4 @@ - Filter diff --git a/core/resources/src/commonMain/composeResources/values-sq/strings.xml b/core/resources/src/commonMain/composeResources/values-sq/strings.xml index edfac59b0..e70391f4d 100644 --- a/core/resources/src/commonMain/composeResources/values-sq/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-sq/strings.xml @@ -186,7 +186,10 @@ Kriptimi me Çelës Publik Përputhje e Gabuar e Çelësit Publik Njoftimet për nyje të reja + 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. + 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. (Cilësia e Ajrit të Brendshëm) shkalla relative e vlerës IAQ siç matet nga Bosch BME680. Intervali i Vlerave 0–500. + Harta e Nyjës Administratë Administratë e Largët I Keq @@ -199,7 +202,6 @@ Hops drejt %1$d Hops prapa %2$d 訊息 Rajon - I shkëputur Koha e skaduar Distanca @@ -216,5 +218,4 @@ - Filtrimi diff --git a/core/resources/src/commonMain/composeResources/values-sr/strings.xml b/core/resources/src/commonMain/composeResources/values-sr/strings.xml index a365fc888..21a9c14c1 100644 --- a/core/resources/src/commonMain/composeResources/values-sr/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-sr/strings.xml @@ -241,9 +241,12 @@ Неусаглашеност јавних кључева Обавештење о новом чвору SNR + Однос сигнал/шум SNR је мера која се користи у комуникацијама за квантитативно одређивање нивоа жељеног сигнала у односу на ниво позадинског шума. У Мештастик и другим бежичним системима, већи SNR указује на јаснији сигнал који може побољшати поузданост и квалитет преноса података. RSSI + 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. (Kvalitet vazduha u zatvorenom prostoru) relativna skala vrednosti IAQ merena Bosch BME680. Raspon vrednosti 0–500. Метрика уређаја + Mapa čvorova Позиција Метрике сензора Administracija @@ -346,8 +349,6 @@ Игнориши MQTT Позитиван за MQTT MQTT подешавања - Raskačeno - Блутут повезан Адреса Корисничко име Лозинка @@ -429,5 +430,4 @@ Блутут Напајано - Filter diff --git a/core/resources/src/commonMain/composeResources/values-srp/strings.xml b/core/resources/src/commonMain/composeResources/values-srp/strings.xml index 5bfbb0a84..516a963f4 100644 --- a/core/resources/src/commonMain/composeResources/values-srp/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-srp/strings.xml @@ -241,9 +241,12 @@ Неусаглашеност јавних кључева Обавештења о новим чворовима SNR + Однос сигнал/шум SNR је мера која се користи у комуникацијама за квантитативно одређивање нивоа жељеног сигнала у односу на ниво позадинског шума. У Мештастик и другим бежичним системима, већи SNR указује на јаснији сигнал који може побољшати поузданост и квалитет преноса података. RSSI + Индикатор јачине примљеног сигнала RSSI је мера која се користи за одређивање нивоа снаге која се прима преко антене. Виша вредност RSSI генерално указује на јачу и стабилнију везу. Индекс квалитета ваздуха (IAQ) као мера за одређивање квалитета ваздуха унутрашњости, мерен са Bosch BME680. Вредности се крећу у распону од 0 до 500. Метрика уређаја + Мапа чворова Позиција Метрике сензора Администрација @@ -346,8 +349,6 @@ Игнориши MQTT Позитиван за MQTT MQTT подешавања - Раскачено - Блутут повезан Адреса Корисничко име Лозинка @@ -429,5 +430,4 @@ Блутут Напајано - Филтер diff --git a/core/resources/src/commonMain/composeResources/values-sv/strings.xml b/core/resources/src/commonMain/composeResources/values-sv/strings.xml index 59e19f1e5..27f368d7e 100644 --- a/core/resources/src/commonMain/composeResources/values-sv/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-sv/strings.xml @@ -43,7 +43,6 @@ Okänd Inväntar kvittens Kvittens köad - Levererad till nät Okänd Kvitterad Ingen rutt @@ -340,9 +339,12 @@ Användarinfo Ny nod avisering SNR + 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. RSSI + 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. (Indoor Air Quality) relativ skala IAQ värdet mätt med Bosch BME600. Värdeintervall 0-500. Enhetens mätvärden + Nod karta Plats Senaste positionsuppdatering Miljövärden @@ -371,7 +373,6 @@ Varaktighet: %1$s s Rutt spårad mot destination:\n\n Rutten spårad tillbaka till oss:\n\n - Inget svar 1h 24T 1V @@ -528,10 +529,6 @@ Ignorera MQTT Ok till MQTT MQTT-konfiguration - Frånkopplad - Ansluten - Testa anslutningen - Anslutningen misslyckades MQTT är aktiverat Adress Användarnamn @@ -949,6 +946,4 @@ Anslut Klart Meshtastic - Filter - Välj enhet diff --git a/core/resources/src/commonMain/composeResources/values-tr/strings.xml b/core/resources/src/commonMain/composeResources/values-tr/strings.xml index 75a9e3a5d..a3ae53c8c 100644 --- a/core/resources/src/commonMain/composeResources/values-tr/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-tr/strings.xml @@ -213,8 +213,11 @@ Genel Anahtar Uyuşmazlığı Yeni düğüm bildirimleri SNR + 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. RSSI + 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. (İç Hava Kalitesi) Bosch BME680 tarafından ölçülen bağıl ölçekli IAQ değeri. Değer Aralığı 0–500. + Düğüm Haritası Konum Yönetim Uzaktan Yönetim @@ -363,8 +366,6 @@ PA fanı devre dışı MQTT'yi Yoksay MQTT Yapılandırması - Bağlantı kesildi - Bağlandı MQTT etkin Adres Kullanıcı adı @@ -546,5 +547,4 @@ Yeşil Bağlan Meshtastic - Filtre diff --git a/core/resources/src/commonMain/composeResources/values-uk/strings.xml b/core/resources/src/commonMain/composeResources/values-uk/strings.xml index c9a86af43..3e96490dc 100644 --- a/core/resources/src/commonMain/composeResources/values-uk/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-uk/strings.xml @@ -277,7 +277,9 @@ Сповіщення про нові вузли SNR RSSI + Показник рівня потужності сигналу — вимірювання, що використовується для визначення рівня потужності, що приймається антеною. Вище значення RSSI зазвичай вказує на міцніше та стабільніше з'єднання. Показники пристрою + Мапа вузлів Місцезнаходження Показники довкілля Адміністрування @@ -399,9 +401,6 @@ Перевизначити частоту Ігнорувати MQTT Налаштування MQTT - Відключено - Під’єднано - Перевірка зʼєднання MQTT увімкнений Адреса Ім'я користувача @@ -723,6 +722,4 @@ Під’єднатися Готово Meshtastic - Фільтри - Оберіть пристрій diff --git a/core/resources/src/commonMain/composeResources/values-zh-rCN/strings.xml b/core/resources/src/commonMain/composeResources/values-zh-rCN/strings.xml index 7fff0db20..87feeb9e2 100644 --- a/core/resources/src/commonMain/composeResources/values-zh-rCN/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-zh-rCN/strings.xml @@ -362,9 +362,12 @@ 用户信息 新节点通知 SNR + 信噪比(Signal-to-Noise Ratio, SNR)是一种用于通信领域的测量指标,用于量化目标信号与背景噪声的比例。在 Meshtastic 及其他无线系统中,较高的信噪比表示信号更加清晰,从而能够提升数据传输的可靠性和质量。 RSSI + 接收信号强度指示(Received Signal Strength Indicator, RSSI)是一种用于测量天线接收到的信号功率的指标。较高的 RSSI 值通常表示更强、更稳定的连接。 室内空气质量(Indoor Air Quality, IAQ):由 Bosch BME680 传感器测量的相对标尺 IAQ 值,取值范围为 0–500。 设备指标 + 节点地图 定位 最后位置更新 传感器指标 @@ -565,9 +568,6 @@ 忽略 MQTT 使用MQTT MQTT设置 - 已断开连接 - 已连接 - 连接测试 启用MQTT 地址 用户名 @@ -1115,6 +1115,4 @@ 连接 完成 Meshtastic - 搜索节点 - 选择设备 diff --git a/core/resources/src/commonMain/composeResources/values-zh-rTW/strings.xml b/core/resources/src/commonMain/composeResources/values-zh-rTW/strings.xml index 20ee6c639..354415089 100644 --- a/core/resources/src/commonMain/composeResources/values-zh-rTW/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-zh-rTW/strings.xml @@ -45,7 +45,6 @@ 無法識別 正在等待確認 發送佇列中 - 已傳送至 Mesh 不明 透過 SF++ 鏈路由… 已在 SF++ 鏈上確認 @@ -123,7 +122,6 @@ 嘗試獲取 GPS 位置的頻率(< 10 秒將保持 GPS 模組開啟)。 位置訊息可選的附加欄位。包含的欄位越多,訊息越大,將造成空中時間拉長和封包遺失的風險增加。 將盡可能使所有元件進入睡眠狀態。對於 tracker 和 sensor 角色,此模式將包含 LoRa 無線電。如果您想搭配手機應用程式使用設備,或正在使用沒有使用者按鈕的設備,請勿啟用此設定。 - 從您的私鑰生成並傳送給網狀網路中的其他節點,以供它們計算出共享密鑰。 用於與遠端設備交換密鑰。 被授權可對此節點發送管理訊息的公鑰。 設備處於受管理狀態,使用者無法變更任何設備設定。 @@ -222,11 +220,6 @@ %1$s: %2$s 來自 %1$s 的訊息:%2$s 標頭 - 標尾 - 點形 - 文字 - 儀表板 - 梯度 這是一個一個一個可客製化的組合元件 還支援多行文字與多種樣式 訊息傳遞狀態 @@ -378,9 +371,12 @@ 使用者資訊 新節點通知 SNR + 信噪比(SNR),用於通訊中量化所需信號與背景噪音水平的指標。在 Meshtastic 及其他無線系統中,信噪比越高表示信號越清晰,可以提高數據傳輸的可靠性和品質。 RSSI + 接收信號強度指示(RSSI)用於測量天線所接收到信號的功率強度。 RSSI 值越高通常代表連線越強且穩定。 (室內空氣品質) 相對尺度 IAQ 值,由 Bosch BME680 測量。值範圍 0–500。 裝置計量資料 + 節點地圖 位置 最後位置更新 環境計量資料 @@ -412,12 +408,6 @@ 回程跳數 來回跳數 無回應 - 1分鐘負載 - 5分鐘負載 - 15分鐘負載 - 1分鐘系統負載平均值 - 5分鐘系統負載平均值 - 15分鐘系統負載平均值 可用系統記憶體(位元組) 1小時 二十四小時 @@ -426,6 +416,7 @@ 1個月 最大值 最小 + 平均 展開圖表 收起圖表 未知年齡 @@ -603,23 +594,6 @@ 無視MQTT 允許轉發至 MQTT MQTT配置 - 已停用 - 已中斷連線 - 已斷線 — %1$s - 正在連接… - 已連線 - 重新連接中… - 重新連接中(第 %1$d 次嘗試) — %2$s - 測試連線 - 正在查詢 Broker… - 可供連線,Broker 已驗證並接受憑證。 - 可供連線(%1$s) - Broker 遭拒:%1$s - 找不到伺服器 - 無法連線至 Broker 中繼伺服器(TCP) - TLS 握手失敗 - 經過 %1$d 毫秒後逾時 - 測試失敗 啟用MQTT服務器 地址 用戶名 @@ -818,9 +792,6 @@ 顯示路徑 顯示定位精準度 客户端通知 - 金鑰驗證 - 金鑰驗證請求 - 金鑰驗證已完成 偵測到重複的公鑰 偵測到加密金鑰強度不足 偵測到金鑰已洩漏,點選確定後重新產生金鑰。 @@ -882,7 +853,6 @@ BLE: %1$s WiFi: %1$s 無可用的 PAX 人流計量資料。 - mPWRD-OS 的 Wi-Fi 設定 藍牙裝置 連接裝置 超過速率限制,請稍後再嘗試。 @@ -1188,9 +1158,6 @@ 保留路由跳數 注意 裝置儲存空間與使用者介面(唯讀) - 主題 %1$s,語言 %2$s - 可使用檔案(%1$d): - - %1$s(%2$d 位元) 未發現任何檔案。 連線 完成 @@ -1199,34 +1166,10 @@ 進一步了解 mPWRD-OS 專案\nhttps://github.com/mPWRD-OS 正在搜尋裝置… 找到裝置 - 準備好掃描 Wi-Fi 網路了。 - 搜尋網路 正在搜尋… - 正在套用 Wi-Fi 設定… 找不到網路 - 無法連接:%1$s - 無法搜尋到 Wi-Fi 網路:%1$s %1$d% 可用的網路 網路名稱(SSID) - 手動輸入或選擇一個網路 - Wi-Fi 已設定完成! - 無法套用 Wi-Fi 設定 - Meshtastic Desktop - 顯示 Meshtastic - 離開 Meshtastic - 匯出 TAK 資料封包 - 清除時區 - 過濾器 - 移除篩選條件 - 顯示空氣品質圖例 - 顯示訊息狀態 - 傳送回覆 - 複製訊息 - 選擇訊息 - 刪除訊息 - 使用表情符號回應 - 選擇裝置 - 選擇網路 diff --git a/core/resources/src/commonMain/composeResources/values/strings.xml b/core/resources/src/commonMain/composeResources/values/strings.xml index 505d80821..4748844c6 100644 --- a/core/resources/src/commonMain/composeResources/values/strings.xml +++ b/core/resources/src/commonMain/composeResources/values/strings.xml @@ -408,9 +408,12 @@ User Info New node notifications SNR + 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. RSSI + 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. (Indoor Air Quality) relative scale IAQ value as measured by Bosch BME680. Value Range 0–500. Device Metrics + Node Map Position Last position update Environment Metrics @@ -457,6 +460,7 @@ 1M Max Min + Avg Expand chart Collapse chart Unknown Age @@ -634,23 +638,6 @@ Ignore MQTT Ok to MQTT MQTT Config - Inactive - Disconnected - Disconnected — %1$s - Connecting… - Connected - Reconnecting… - Reconnecting (attempt %1$d) — %2$s - Test connection - Probing broker… - Reachable. Broker accepted credentials. - Reachable (%1$s) - Broker rejected: %1$s - Host not found - Cannot reach broker (TCP) - TLS handshake failed - Timed out after %1$d ms - Connection failed MQTT enabled Address Username @@ -1278,18 +1265,4 @@ Show Meshtastic Quit Meshtastic - Export TAK Data Package - mPWRD-OS - Clear time zone - Filter - Remove filter - Show air quality legend - Show message status - Send reply - Copy message - Select message - Delete message - React with emoji - Select device - Select network diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeDeviceHardwareRepository.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeDeviceHardwareRepository.kt deleted file mode 100644 index ef8cac0ba..000000000 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeDeviceHardwareRepository.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -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, Result>() - private val calls = mutableListOf>() - - init { - registerResetAction { - hardware.clear() - calls.clear() - } - } - - /** Records every [getDeviceHardwareByModel] invocation for assertion. */ - val recordedCalls: List> - get() = calls.toList() - - override suspend fun getDeviceHardwareByModel( - hwModel: Int, - target: String?, - forceRefresh: Boolean, - ): Result { - 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) { - hardware[hwModel to target] = result - } -} diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeFirmwareReleaseRepository.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeFirmwareReleaseRepository.kt deleted file mode 100644 index 166256764..000000000 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeFirmwareReleaseRepository.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -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(null) - private val _alphaRelease = mutableStateFlow(null) - - override val stableRelease: Flow = _stableRelease - override val alphaRelease: Flow = _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 - } -} diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeQuickChatActionRepository.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeQuickChatActionRepository.kt deleted file mode 100644 index 215542485..000000000 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeQuickChatActionRepository.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -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>(emptyList()) - - override fun getAllActions(): Flow> = 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) { - actionsFlow.value = actions.sortedBy { it.position } - } - - /** Returns the current in-memory snapshot. */ - val currentActions: List - get() = actionsFlow.value -} diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioConfigRepository.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioConfigRepository.kt deleted file mode 100644 index aa68e9b21..000000000 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioConfigRepository.kt +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -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 = channelSetBacking - - private val localConfigBacking = mutableStateFlow(LocalConfig()) - override val localConfigFlow: Flow = localConfigBacking - - private val moduleConfigBacking = mutableStateFlow(LocalModuleConfig()) - override val moduleConfigFlow: Flow = moduleConfigBacking - - private val deviceProfileBacking = mutableStateFlow(DeviceProfile()) - override val deviceProfileFlow: Flow = deviceProfileBacking - val currentDeviceProfile: DeviceProfile - get() = deviceProfileBacking.value - - private val deviceUIConfigBacking = mutableStateFlow(null) - override val deviceUIConfigFlow: Flow = deviceUIConfigBacking - - private val fileManifestBacking = mutableStateFlow>(emptyList()) - override val fileManifestFlow: Flow> = 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 - 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) { - 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 - } -} diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeTracerouteSnapshotRepository.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeTracerouteSnapshotRepository.kt deleted file mode 100644 index a52b86bd0..000000000 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeTracerouteSnapshotRepository.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -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>>(emptyMap()) - private val requestIds = mutableMapOf() - - init { - registerResetAction { requestIds.clear() } - } - - override fun getSnapshotPositions(logUuid: String): Flow> = - snapshots.map { it[logUuid].orEmpty() } - - override suspend fun upsertSnapshotPositions(logUuid: String, requestId: Int, positions: Map) { - 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) { - 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] -} diff --git a/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/RepositoryFakesTest.kt b/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/RepositoryFakesTest.kt deleted file mode 100644 index f9a63c712..000000000 --- a/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/RepositoryFakesTest.kt +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -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()) - } -} 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 5365ab95e..231c84d40 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(ioDispatcher) { + withContext(Dispatchers.IO) { @Suppress("TooGenericExceptionCaught") try { val androidUri = uri.toAndroidUri() diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ClickableTextField.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ClickableTextField.kt index 125e1e117..7330c1aa6 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ClickableTextField.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ClickableTextField.kt @@ -38,7 +38,6 @@ fun ClickableTextField( onClick: () -> Unit, modifier: Modifier = Modifier, isError: Boolean = false, - trailingIconContentDescription: String? = null, ) { val source = remember { MutableInteractionSource() } val isPressed by source.collectIsPressedAsState() @@ -50,7 +49,7 @@ fun ClickableTextField( enabled = enabled, readOnly = true, label = { Text(stringResource(label)) }, - trailingIcon = { Icon(trailingIcon, trailingIconContentDescription) }, + trailingIcon = { Icon(trailingIcon, null) }, isError = isError, interactionSource = source, modifier = modifier, diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/IndoorAirQuality.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/IndoorAirQuality.kt index 2fa66b468..b84c11e13 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/IndoorAirQuality.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/IndoorAirQuality.kt @@ -44,7 +44,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -59,7 +58,6 @@ import org.meshtastic.core.resources.preview_gauge import org.meshtastic.core.resources.preview_gradient import org.meshtastic.core.resources.preview_pill 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.ThumbUp import org.meshtastic.core.ui.icon.Warning @@ -122,18 +120,13 @@ fun IndoorAirQuality(iaq: Int?, displayMode: IaqDisplayMode = IaqDisplayMode.Pil Column { when (displayMode) { IaqDisplayMode.Pill -> { - val legendLabel = stringResource(Res.string.show_iaq_legend) Box( modifier = Modifier.clip(RoundedCornerShape(10.dp)) .background(iaqEnum.color) .width(125.dp) .height(30.dp) - .clickable( - onClickLabel = legendLabel, - role = Role.Button, - onClick = { isLegendOpen = true }, - ), + .clickable { isLegendOpen = true }, ) { Row( modifier = Modifier.padding(4.dp).align(Alignment.CenterStart), @@ -151,15 +144,7 @@ fun IndoorAirQuality(iaq: Int?, displayMode: IaqDisplayMode = IaqDisplayMode.Pil } IaqDisplayMode.Dot -> { - val legendLabel = stringResource(Res.string.show_iaq_legend) - Column( - modifier = - Modifier.clickable( - onClickLabel = legendLabel, - role = Role.Button, - onClick = { isLegendOpen = true }, - ), - ) { + Column(modifier = Modifier.clickable { isLegendOpen = true }) { Row(verticalAlignment = Alignment.CenterVertically) { Text(text = "$iaq") Spacer(modifier = Modifier.width(4.dp)) @@ -169,30 +154,17 @@ fun IndoorAirQuality(iaq: Int?, displayMode: IaqDisplayMode = IaqDisplayMode.Pil } IaqDisplayMode.Text -> { - val legendLabel = stringResource(Res.string.show_iaq_legend) Text( text = getIaqDescriptionWithRange(iaqEnum), fontSize = 12.sp, - modifier = - Modifier.clickable( - onClickLabel = legendLabel, - role = Role.Button, - onClick = { isLegendOpen = true }, - ), + modifier = Modifier.clickable { isLegendOpen = true }, ) } IaqDisplayMode.Gauge -> { - val legendLabel = stringResource(Res.string.show_iaq_legend) CircularProgressIndicator( progress = { iaq / 500f }, - modifier = - Modifier.size(60.dp) - .clickable( - onClickLabel = legendLabel, - role = Role.Button, - onClick = { isLegendOpen = true }, - ), + modifier = Modifier.size(60.dp).clickable { isLegendOpen = true }, strokeWidth = 8.dp, color = iaqEnum.color, ) @@ -200,15 +172,9 @@ fun IndoorAirQuality(iaq: Int?, displayMode: IaqDisplayMode = IaqDisplayMode.Pil } IaqDisplayMode.Gradient -> { - val legendLabel = stringResource(Res.string.show_iaq_legend) Row( horizontalArrangement = Arrangement.SpaceBetween, - modifier = - Modifier.clickable( - onClickLabel = legendLabel, - role = Role.Button, - onClick = { isLegendOpen = true }, - ), + modifier = Modifier.clickable { isLegendOpen = true }, ) { LinearProgressIndicator( progress = { iaq / 500f }, diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/RegularPreference.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/RegularPreference.kt index f9f839ea5..afa82460d 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/RegularPreference.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/RegularPreference.kt @@ -34,7 +34,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -81,13 +80,7 @@ fun RegularPreference( MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) } - Column( - modifier = - modifier - .fillMaxWidth() - .clickable(enabled = enabled, onClick = onClick, role = Role.Button) - .padding(all = 16.dp), - ) { + Column(modifier = modifier.fillMaxWidth().clickable(enabled = enabled, onClick = onClick).padding(all = 16.dp)) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) { FlowRow(modifier = Modifier.weight(1f), horizontalArrangement = Arrangement.SpaceBetween) { Text( 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 a938f92ea..031e1fe35 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(ioDispatcher) { + withContext(Dispatchers.IO) { @Suppress("TooGenericExceptionCaught") try { val file = File(URI(uri.toString())) diff --git a/desktop/proguard-rules.pro b/desktop/proguard-rules.pro index 280214b2e..9e23e32c7 100644 --- a/desktop/proguard-rules.pro +++ b/desktop/proguard-rules.pro @@ -46,9 +46,8 @@ -keep class org.meshtastic.desktop.MainKt { *; } # ---- Ktor Java engine (desktop-only; Android uses OkHttp) ------------------- -# io.ktor.client.engine.java ships consumer rules; the shared -# HttpClientEngineFactory ServiceLoader keep in shared-rules.pro covers the -# reflective discovery path. + +-keep class io.ktor.client.engine.java.** { *; } # ---- Meshtastic desktop host shell ------------------------------------------ diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt b/desktop/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt index 707dfaf03..985a76987 100644 --- a/desktop/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt +++ b/desktop/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt @@ -44,7 +44,6 @@ import org.meshtastic.core.repository.PlatformAnalytics import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.repository.ServiceBroadcasts import org.meshtastic.proto.MqttClientProxyMessage -import org.meshtastic.mqtt.ConnectionState as MqttConnectionState import org.meshtastic.proto.Position as ProtoPosition /** @@ -163,8 +162,6 @@ class NoopMQTTRepository : MQTTRepository { override val proxyMessageFlow: Flow = emptyFlow() override fun publish(topic: String, data: ByteArray, retained: Boolean) {} - - override val connectionState = MutableStateFlow(MqttConnectionState.Disconnected.Idle) } // endregion diff --git a/fastlane/metadata/android/fr-FR/changelogs/default.txt b/fastlane/metadata/android/fr-FR/changelogs/default.txt index a322da020..0553de284 100644 --- a/fastlane/metadata/android/fr-FR/changelogs/default.txt +++ b/fastlane/metadata/android/fr-FR/changelogs/default.txt @@ -1 +1 @@ -Pour des notes de version détaillées, veuillez visiter : https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file +For detailed release notes, please visit: https://github.com/meshtastic/Meshtastic-Android/releases/ \ No newline at end of file diff --git a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/domain/usecase/AndroidGetDiscoveredDevicesUseCase.kt b/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/domain/usecase/AndroidGetDiscoveredDevicesUseCase.kt index b6999aadc..b0a3d738c 100644 --- a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/domain/usecase/AndroidGetDiscoveredDevicesUseCase.kt +++ b/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/domain/usecase/AndroidGetDiscoveredDevicesUseCase.kt @@ -60,14 +60,7 @@ class AndroidGetDiscoveredDevicesUseCase( override fun invoke(showMock: Boolean): Flow { val nodeDb = nodeRepository.nodeDBbyNum - // 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 bondedBleFlow = bluetoothRepository.state.map { ble -> ble.bondedDevices.map { DeviceListEntry.Ble(it) } } val processedTcpFlow = combine(networkRepository.resolvedList, recentAddressesDataSource.recentAddresses) { diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/DeviceListItem.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/DeviceListItem.kt index 14f4dc42b..a4d8ecdd8 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/DeviceListItem.kt +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/DeviceListItem.kt @@ -17,13 +17,13 @@ package org.meshtastic.feature.connections.ui.components import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.selection.selectable import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.ListItem @@ -41,15 +41,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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 kotlinx.coroutines.delay import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.model.ConnectionState 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.bluetooth import org.meshtastic.core.resources.network @@ -112,19 +108,11 @@ fun DeviceListItem( is DeviceListEntry.Mock -> stringResource(Res.string.add) } - val selectLabel = stringResource(Res.string.action_select_device) - val isSelected = connectionState is ConnectionState.Connected val clickableModifier = if (onDelete != null) { - Modifier.semantics { selected = isSelected } - .combinedClickable( - onClickLabel = selectLabel, - role = Role.RadioButton, - onClick = onSelect, - onLongClick = onDelete, - ) + Modifier.combinedClickable(onClick = onSelect, onLongClick = onDelete) } else { - Modifier.selectable(selected = isSelected, role = Role.RadioButton, onClick = onSelect) + Modifier.clickable(onClick = onSelect) } ListItem( 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 f8ff9fcac..dc1c45971 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,7 +35,6 @@ 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 @@ -92,7 +91,6 @@ class FirmwareUpdateViewModel( private val firmwareUpdateManager: FirmwareUpdateManager, private val usbManager: FirmwareUsbManager, private val fileHandler: FirmwareFileHandler, - private val applicationScope: ApplicationCoroutineScope, ) : ViewModel() { private val _state = MutableStateFlow(FirmwareUpdateState.Idle) @@ -126,10 +124,12 @@ class FirmwareUpdateViewModel( override fun onCleared() { super.onCleared() - // 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) { + // 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) { 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 030d84eff..4c48a1ced 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,7 +108,6 @@ 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 a8eddff83..7032ed408 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,7 +124,6 @@ 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 deleted file mode 100644 index 3ef5c44ef..000000000 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/TestApplicationCoroutineScope.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -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 23a0d03ab..acb1545bd 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,7 +116,6 @@ class FirmwareUpdateViewModelFileTest { firmwareUpdateManager, usbManager, fileHandler, - TestApplicationCoroutineScope(testDispatcher), ) // ----------------------------------------------------------------------- diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageActionsBottomSheet.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageActionsBottomSheet.kt index 5ffb5ea1d..c4c99720c 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageActionsBottomSheet.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageActionsBottomSheet.kt @@ -36,18 +36,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.model.MessageStatus 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.delete import org.meshtastic.core.resources.device_metrics_label_value @@ -62,7 +55,6 @@ import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.Reply import org.meshtastic.core.ui.icon.SelectAll -@Suppress("LongMethod") @Composable fun MessageActionsContent( quickEmojis: List, @@ -91,35 +83,20 @@ fun MessageActionsContent( Text(stringResource(Res.string.device_metrics_label_value, title, statusText.orEmpty())) }, leadingContent = { MessageStatusIcon(status = status) }, - modifier = - Modifier.clickable( - onClickLabel = stringResource(Res.string.action_show_message_status), - role = Role.Button, - onClick = onStatus, - ), + modifier = Modifier.clickable(onClick = onStatus), ) } ListItem( headlineContent = { Text(stringResource(Res.string.reply)) }, leadingContent = { Icon(MeshtasticIcons.Reply, contentDescription = stringResource(Res.string.reply)) }, - modifier = - Modifier.clickable( - onClickLabel = stringResource(Res.string.action_send_reply), - role = Role.Button, - onClick = onReply, - ), + modifier = Modifier.clickable(onClick = onReply), ) ListItem( headlineContent = { Text(stringResource(Res.string.copy)) }, leadingContent = { Icon(MeshtasticIcons.Copy, contentDescription = stringResource(Res.string.copy)) }, - modifier = - Modifier.clickable( - onClickLabel = stringResource(Res.string.action_copy_message), - role = Role.Button, - onClick = onCopy, - ), + modifier = Modifier.clickable(onClick = onCopy), ) ListItem( @@ -127,23 +104,13 @@ fun MessageActionsContent( leadingContent = { Icon(MeshtasticIcons.SelectAll, contentDescription = stringResource(Res.string.select)) }, - modifier = - Modifier.clickable( - onClickLabel = stringResource(Res.string.action_select_message), - role = Role.Button, - onClick = onSelect, - ), + modifier = Modifier.clickable(onClick = onSelect), ) ListItem( headlineContent = { Text(stringResource(Res.string.delete)) }, leadingContent = { Icon(MeshtasticIcons.Delete, contentDescription = stringResource(Res.string.delete)) }, - modifier = - Modifier.clickable( - onClickLabel = stringResource(Res.string.action_delete_message), - role = Role.Button, - onClick = onDelete, - ), + modifier = Modifier.clickable(onClick = onDelete), ) } } @@ -163,12 +130,7 @@ private fun QuickEmojiRow(quickEmojis: List, onReact: (String) -> Unit, Modifier.size(40.dp) .clip(CircleShape) .background(MaterialTheme.colorScheme.surfaceVariant) - .clickable( - onClickLabel = stringResource(Res.string.action_react_with_emoji), - role = Role.Button, - ) { - onReact(emoji) - }, + .clickable { onReact(emoji) }, contentAlignment = Alignment.Center, ) { Text(text = emoji, style = MaterialTheme.typography.titleMedium) diff --git a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt index 9b8267793..27797592b 100644 --- a/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt +++ b/feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/Reaction.kt @@ -143,7 +143,7 @@ internal fun ReactionRow( AnimatedVisibility(emojiGroups.isNotEmpty(), modifier = modifier) { LazyRow(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { - items(emojiGroups.entries.toList(), key = { it.key }) { entry -> + items(emojiGroups.entries.toList()) { entry -> val emoji = entry.key val reactions = entry.value 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()) { - items(groupedEmojis.entries.toList(), key = { it.key }) { entry -> + items(groupedEmojis.entries.toList()) { entry -> val emoji = entry.key val reactions = entry.value 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)) LazyColumn(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(4.dp)) { - items(filteredReactions, key = { reaction -> "${reaction.user.id}:${reaction.emoji}" }) { reaction -> + items(filteredReactions) { reaction -> Column(modifier = Modifier.padding(horizontal = 8.dp)) { Row( modifier = Modifier.fillMaxWidth(), diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt index 0bc022c34..cfac18158 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeFilterTextField.kt @@ -49,7 +49,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.style.TextAlign @@ -57,7 +56,6 @@ import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.model.NodeSortOption 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.node_filter_exclude_infrastructure import org.meshtastic.core.resources.node_filter_exclude_mqtt @@ -180,19 +178,14 @@ private fun NodeFilterTextField(filterText: String, onTextChange: (String) -> Un onValueChange = onTextChange, trailingIcon = { if (filterText.isNotEmpty() || isFocused) { - val clearLabel = stringResource(Res.string.clear) Icon( MeshtasticIcons.Close, contentDescription = stringResource(Res.string.desc_node_filter_clear), modifier = - Modifier.clickable( - onClickLabel = clearLabel, - role = Role.Button, - onClick = { - onTextChange("") - focusManager.clearFocus() - }, - ), + Modifier.clickable { + onTextChange("") + focusManager.clearFocus() + }, ) } }, diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/HandleNodeAction.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/HandleNodeAction.kt index 559582417..9ce025604 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/HandleNodeAction.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/HandleNodeAction.kt @@ -43,7 +43,10 @@ internal fun handleNodeAction( val route = viewModel.getDirectMessageRoute(menuAction.node, uiState.ourNode) navigateToMessages(route) } - is NodeMenuAction.Remove -> viewModel.handleNodeMenuAction(menuAction, onNavigateUp) + is NodeMenuAction.Remove -> { + viewModel.handleNodeMenuAction(menuAction) + onNavigateUp() + } else -> viewModel.handleNodeMenuAction(menuAction) } } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt index e891d8ae0..733cd858c 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt @@ -89,10 +89,9 @@ class NodeDetailViewModel( } /** Dispatches high-level node management actions like removal, muting, or favoriting. */ - fun handleNodeMenuAction(action: NodeMenuAction, onAfterRemove: () -> Unit = {}) { + fun handleNodeMenuAction(action: NodeMenuAction) { when (action) { - is NodeMenuAction.Remove -> - nodeManagementActions.requestRemoveNode(viewModelScope, action.node, onAfterRemove) + is NodeMenuAction.Remove -> nodeManagementActions.requestRemoveNode(viewModelScope, action.node) is NodeMenuAction.Ignore -> nodeManagementActions.requestIgnoreNode(viewModelScope, action.node) is NodeMenuAction.Mute -> nodeManagementActions.requestMuteNode(viewModelScope, action.node) is NodeMenuAction.Favorite -> nodeManagementActions.requestFavoriteNode(viewModelScope, action.node) diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt index 9c021e666..436954201 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt @@ -50,14 +50,11 @@ constructor( private val radioController: RadioController, private val alertManager: AlertManager, ) { - open fun requestRemoveNode(scope: CoroutineScope, node: Node, onAfterRemove: () -> Unit = {}) { + open fun requestRemoveNode(scope: CoroutineScope, node: Node) { alertManager.showAlert( titleRes = Res.string.remove, messageRes = Res.string.remove_node_text, - onConfirm = { - removeNode(scope, node.num) - onAfterRemove() - }, + onConfirm = { removeNode(scope, node.num) }, ) } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt index d09bdc8d1..77c6781f1 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt @@ -42,7 +42,6 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.DateFormatter -import org.meshtastic.core.common.util.MetricFormatter import org.meshtastic.core.common.util.formatString import org.meshtastic.core.common.util.nowSeconds import org.meshtastic.core.model.TelemetryType @@ -166,10 +165,7 @@ private fun HumidityAndBarometricPressureDisplay(envMetrics: org.meshtastic.prot MetricIndicator(Environment.HUMIDITY.color) Spacer(Modifier.width(4.dp)) Text( - text = - "${stringResource( - Res.string.humidity, - )} ${MetricFormatter.percent(humidity, decimalPlaces = 2)}", + text = formatString("%s %.2f%%", stringResource(Res.string.humidity), humidity), color = MaterialTheme.colorScheme.onSurface, fontSize = MaterialTheme.typography.labelLarge.fontSize, modifier = Modifier.padding(vertical = 0.dp), @@ -182,7 +178,7 @@ private fun HumidityAndBarometricPressureDisplay(envMetrics: org.meshtastic.prot MetricIndicator(Environment.BAROMETRIC_PRESSURE.color) Spacer(Modifier.width(4.dp)) Text( - text = MetricFormatter.pressure(pressure, decimalPlaces = 2), + text = formatString("%.2f hPa", pressure), color = MaterialTheme.colorScheme.onSurface, fontSize = MaterialTheme.typography.labelLarge.fontSize, modifier = Modifier.padding(vertical = 0.dp), @@ -290,7 +286,7 @@ private fun VoltageCurrentDisplay(envMetrics: org.meshtastic.proto.EnvironmentMe if (hasVoltage) { val voltage = envMetrics.voltage!! Text( - text = "${stringResource(Res.string.voltage)} ${MetricFormatter.voltage(voltage)}", + text = formatString("%s %.2f V", stringResource(Res.string.voltage), voltage), color = MaterialTheme.colorScheme.onSurface, fontSize = MaterialTheme.typography.labelLarge.fontSize, ) @@ -298,10 +294,7 @@ private fun VoltageCurrentDisplay(envMetrics: org.meshtastic.proto.EnvironmentMe if (hasCurrent) { val currentValue = envMetrics.current!! Text( - text = - "${stringResource( - Res.string.current, - )} ${MetricFormatter.current(currentValue, decimalPlaces = 2)}", + text = formatString("%s %.2f mA", stringResource(Res.string.current), currentValue), color = MaterialTheme.colorScheme.onSurface, fontSize = MaterialTheme.typography.labelLarge.fontSize, ) @@ -394,11 +387,7 @@ private fun WindSpeedRow(envMetrics: org.meshtastic.proto.EnvironmentMetrics) { envMetrics.wind_direction!!, ) } else { - formatString( - "%s %s", - stringResource(Res.string.wind_speed), - MetricFormatter.windSpeed(envMetrics.wind_speed!!), - ) + formatString("%s %.1f m/s", stringResource(Res.string.wind_speed), envMetrics.wind_speed!!) } Text( text = dirText, @@ -414,14 +403,14 @@ private fun WindGustLullRow(envMetrics: org.meshtastic.proto.EnvironmentMetrics, Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { if (hasGust) { Text( - text = "${stringResource(Res.string.wind_gust)} ${MetricFormatter.windSpeed(envMetrics.wind_gust!!)}", + text = formatString("%s %.1f m/s", stringResource(Res.string.wind_gust), envMetrics.wind_gust!!), color = MaterialTheme.colorScheme.onSurface, fontSize = MaterialTheme.typography.labelLarge.fontSize, ) } if (hasLull) { Text( - text = "${stringResource(Res.string.wind_lull)} ${MetricFormatter.windSpeed(envMetrics.wind_lull!!)}", + text = formatString("%s %.1f m/s", stringResource(Res.string.wind_lull), envMetrics.wind_lull!!), color = MaterialTheme.colorScheme.onSurface, fontSize = MaterialTheme.typography.labelLarge.fontSize, ) @@ -438,10 +427,7 @@ private fun RainfallDisplay(envMetrics: org.meshtastic.proto.EnvironmentMetrics) Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { if (has1h) { Text( - text = - "${stringResource( - Res.string.rainfall_1h, - )} ${MetricFormatter.rainfall(envMetrics.rainfall_1h!!)}", + text = formatString("%s %.1f mm", stringResource(Res.string.rainfall_1h), envMetrics.rainfall_1h!!), color = MaterialTheme.colorScheme.onSurface, fontSize = MaterialTheme.typography.labelLarge.fontSize, ) @@ -449,9 +435,7 @@ private fun RainfallDisplay(envMetrics: org.meshtastic.proto.EnvironmentMetrics) if (has24h) { Text( text = - "${stringResource( - Res.string.rainfall_24h, - )} ${MetricFormatter.rainfall(envMetrics.rainfall_24h!!)}", + formatString("%s %.1f mm", stringResource(Res.string.rainfall_24h), envMetrics.rainfall_24h!!), color = MaterialTheme.colorScheme.onSurface, fontSize = MaterialTheme.typography.labelLarge.fontSize, ) diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/HandleNodeActionTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/HandleNodeActionTest.kt deleted file mode 100644 index 6bca8822b..000000000 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/HandleNodeActionTest.kt +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.feature.node.detail - -import androidx.lifecycle.SavedStateHandle -import dev.mokkery.answering.returns -import dev.mokkery.every -import dev.mokkery.matcher.any -import dev.mokkery.mock -import dev.mokkery.verify -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain -import org.meshtastic.core.model.Node -import org.meshtastic.core.repository.ServiceRepository -import org.meshtastic.feature.node.component.NodeMenuAction -import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase -import org.meshtastic.feature.node.model.NodeDetailAction -import org.meshtastic.proto.User -import kotlin.test.AfterTest -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertFalse - -@OptIn(ExperimentalCoroutinesApi::class) -class HandleNodeActionTest { - - private val testDispatcher = UnconfinedTestDispatcher() - private val nodeManagementActions: NodeManagementActions = mock() - private val nodeRequestActions: NodeRequestActions = mock() - private val serviceRepository: ServiceRepository = mock() - private val getNodeDetailsUseCase: GetNodeDetailsUseCase = mock() - - @BeforeTest - fun setUp() { - Dispatchers.setMain(testDispatcher) - every { getNodeDetailsUseCase(any()) } returns emptyFlow() - } - - @AfterTest - fun tearDown() { - Dispatchers.resetMain() - } - - @Test - fun `remove action delegates to viewModel and does not navigate up immediately`() = runTest(testDispatcher) { - val node = Node(num = 1234, user = User(id = "!1234")) - every { nodeManagementActions.requestRemoveNode(any(), any(), any()) } returns Unit - val viewModel = createViewModel() - var navigateUpCalled = false - - handleNodeAction( - action = NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.Remove(node)), - uiState = NodeDetailUiState(), - navigateToMessages = {}, - onNavigateUp = { navigateUpCalled = true }, - onNavigate = {}, - viewModel = viewModel, - ) - - verify { nodeManagementActions.requestRemoveNode(any(), node, any()) } - assertFalse(navigateUpCalled) - } - - private fun createViewModel() = NodeDetailViewModel( - savedStateHandle = SavedStateHandle(mapOf("destNum" to 1234)), - nodeManagementActions = nodeManagementActions, - nodeRequestActions = nodeRequestActions, - serviceRepository = serviceRepository, - getNodeDetailsUseCase = getNodeDetailsUseCase, - ) -} diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt index 3212a313e..89015c807 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt @@ -30,7 +30,6 @@ import org.meshtastic.core.testing.FakeRadioController import org.meshtastic.core.ui.util.AlertManager import org.meshtastic.proto.User import kotlin.test.Test -import kotlin.test.assertTrue @OptIn(ExperimentalCoroutinesApi::class) class NodeManagementActionsTest { @@ -70,23 +69,4 @@ class NodeManagementActionsTest { ) } } - - @Test - fun requestRemoveNode_invokes_onAfterRemove_when_user_confirms() { - val realAlertManager = AlertManager() - val actionsWithRealAlert = - NodeManagementActions( - nodeRepository = nodeRepository, - serviceRepository = serviceRepository, - radioController = radioController, - alertManager = realAlertManager, - ) - val node = Node(num = 123, user = User(long_name = "Test Node")) - var afterRemoveCalled = false - - actionsWithRealAlert.requestRemoveNode(testScope, node) { afterRemoveCalled = true } - realAlertManager.currentAlert.value?.onConfirm?.invoke() - - assertTrue(afterRemoveCalled) - } } 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 315ad1da8..c251b4d5e 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,7 +27,6 @@ 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 @@ -49,7 +48,7 @@ actual fun rememberLogExporter(logsProvider: suspend () -> List) = - withContext(ioDispatcher) { + withContext(Dispatchers.IO) { 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 a28a57678..9afde85e5 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(ioDispatcher) { +private suspend fun exportZipToUri(context: Context, targetUri: Uri, data: ByteArray) = withContext(Dispatchers.IO) { 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/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt index df4a0965f..37cdeab71 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugFilters.kt @@ -57,10 +57,8 @@ import org.meshtastic.core.resources.debug_filter_clear import org.meshtastic.core.resources.debug_filter_included import org.meshtastic.core.resources.debug_filter_preset_title import org.meshtastic.core.resources.debug_filters -import org.meshtastic.core.resources.filter_icon import org.meshtastic.core.resources.match_all import org.meshtastic.core.resources.match_any -import org.meshtastic.core.resources.remove_filter import org.meshtastic.core.ui.icon.Add import org.meshtastic.core.ui.icon.Check import org.meshtastic.core.ui.icon.Close @@ -283,18 +281,8 @@ fun DebugActiveFilters( selected = true, onClick = { onFilterTextsChange(filterTexts - filter) }, label = { Text(filter) }, - leadingIcon = { - Icon( - imageVector = MeshtasticIcons.FilterAlt, - contentDescription = stringResource(Res.string.filter_icon), - ) - }, - trailingIcon = { - Icon( - imageVector = MeshtasticIcons.Close, - contentDescription = stringResource(Res.string.remove_filter), - ) - }, + leadingIcon = { Icon(imageVector = MeshtasticIcons.FilterAlt, contentDescription = null) }, + trailingIcon = { Icon(imageVector = MeshtasticIcons.Close, contentDescription = null) }, ) } } diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt index c59f00b56..7a946b78b 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt @@ -20,17 +20,14 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import co.touchlab.kermit.Logger -import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch import org.jetbrains.compose.resources.StringResource import org.koin.core.annotation.InjectedParam import org.koin.core.annotation.KoinViewModel @@ -46,8 +43,6 @@ import org.meshtastic.core.domain.usecase.settings.RadioResponseResult import org.meshtastic.core.domain.usecase.settings.ToggleAnalyticsUseCase import org.meshtastic.core.domain.usecase.settings.ToggleHomoglyphEncodingUseCase import org.meshtastic.core.model.ConnectionState -import org.meshtastic.core.model.MqttConnectionState -import org.meshtastic.core.model.MqttProbeStatus import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.model.Node import org.meshtastic.core.model.Position @@ -57,7 +52,6 @@ import org.meshtastic.core.repository.HomoglyphPrefs import org.meshtastic.core.repository.LocationRepository import org.meshtastic.core.repository.LocationService import org.meshtastic.core.repository.MapConsentPrefs -import org.meshtastic.core.repository.MqttManager import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository @@ -131,7 +125,6 @@ open class RadioConfigViewModel( private val processRadioResponseUseCase: ProcessRadioResponseUseCase, private val locationService: LocationService, private val fileService: FileService, - private val mqttManager: MqttManager, ) : ViewModel() { val analyticsAllowedFlow = analyticsPrefs.analyticsAllowed @@ -145,41 +138,6 @@ open class RadioConfigViewModel( toggleHomoglyphEncodingUseCase() } - /** MQTT proxy connection state for the settings UI. */ - val mqttConnectionState: StateFlow = mqttManager.mqttConnectionState - - private val _mqttProbeStatus = MutableStateFlow(null) - - /** Latest result from a [probeMqttConnection] call, or `null` if no probe has been run. */ - val mqttProbeStatus: StateFlow = _mqttProbeStatus.asStateFlow() - - private var probeJob: Job? = null - - /** - * Run a one-shot reachability/credentials probe against an MQTT broker. Cancels any in-flight probe before starting - * a new one. Result is exposed via [mqttProbeStatus]. - */ - fun probeMqttConnection(address: String, tlsEnabled: Boolean, username: String?, password: String?) { - probeJob?.cancel() - _mqttProbeStatus.value = MqttProbeStatus.Probing - probeJob = - viewModelScope.launch { - val result = - runCatching { mqttManager.probe(address, tlsEnabled, username, password) } - .getOrElse { e -> - Logger.w(e) { "MQTT probe threw" } - MqttProbeStatus.Other(message = e.message) - } - _mqttProbeStatus.value = result - } - } - - /** Clear the latest probe result (e.g. when the user edits the address). */ - fun clearMqttProbeStatus() { - probeJob?.cancel() - _mqttProbeStatus.value = null - } - private val destNumFlow = MutableStateFlow(savedStateHandle.get("destNum")) fun initDestNum(id: Int?) { diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigScreen.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigScreen.kt index a614c1f99..c65cd971b 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigScreen.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/DeviceConfigScreen.kt @@ -59,7 +59,6 @@ import org.meshtastic.core.resources.are_you_sure import org.meshtastic.core.resources.button_gpio import org.meshtastic.core.resources.buzzer_gpio import org.meshtastic.core.resources.cancel -import org.meshtastic.core.resources.clear_time_zone import org.meshtastic.core.resources.config_device_doubleTapAsButtonPress_summary import org.meshtastic.core.resources.config_device_ledHeartbeatEnabled_summary import org.meshtastic.core.resources.config_device_tripleClickAsAdHocPing_summary @@ -270,10 +269,7 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit onValueChanged = { formState.value = formState.value.copy(tzdef = it) }, trailingIcon = { IconButton(onClick = { formState.value = formState.value.copy(tzdef = "") }) { - Icon( - imageVector = MeshtasticIcons.Close, - contentDescription = stringResource(Res.string.clear_time_zone), - ) + Icon(imageVector = MeshtasticIcons.Close, contentDescription = null) } }, ) @@ -286,10 +282,7 @@ fun DeviceConfigScreenCommon(viewModel: RadioConfigViewModel, onBack: () -> Unit shape = RectangleShape, onClick = { formState.value = formState.value.copy(tzdef = appTzPosixString) }, ) { - Icon( - imageVector = MeshtasticIcons.PhoneAndroid, - contentDescription = stringResource(Res.string.config_device_use_phone_tz), - ) + Icon(imageVector = MeshtasticIcons.PhoneAndroid, contentDescription = null) Spacer(modifier = Modifier.width(8.dp)) diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/LoadingOverlay.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/LoadingOverlay.kt index 2646b20cb..8039dc37d 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/LoadingOverlay.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/LoadingOverlay.kt @@ -39,7 +39,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import org.meshtastic.core.common.util.MetricFormatter +import org.meshtastic.core.common.util.formatString import org.meshtastic.feature.settings.radio.ResponseState private const val LOADING_OVERLAY_ALPHA = 0.8f @@ -73,7 +73,7 @@ fun LoadingOverlay(state: ResponseState<*>, modifier: Modifier = Modifier) { trackColor = MaterialTheme.colorScheme.surfaceVariant, ) Text( - text = MetricFormatter.percent(progress * PERCENTAGE_FACTOR, decimalPlaces = 0), + text = formatString("%.0f%%", progress * PERCENTAGE_FACTOR), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, ) diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt index e1f407679..0427f9520 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt @@ -18,37 +18,17 @@ package org.meshtastic.feature.settings.radio.component -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Button import androidx.compose.material3.CardDefaults import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusManager -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.model.MqttConnectionState -import org.meshtastic.core.model.MqttProbeStatus import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.address import org.meshtastic.core.resources.default_mqtt_address @@ -58,23 +38,6 @@ import org.meshtastic.core.resources.map_reporting import org.meshtastic.core.resources.mqtt import org.meshtastic.core.resources.mqtt_config import org.meshtastic.core.resources.mqtt_enabled -import org.meshtastic.core.resources.mqtt_probe_dns_failure -import org.meshtastic.core.resources.mqtt_probe_other_failure -import org.meshtastic.core.resources.mqtt_probe_rejected -import org.meshtastic.core.resources.mqtt_probe_running -import org.meshtastic.core.resources.mqtt_probe_success -import org.meshtastic.core.resources.mqtt_probe_success_with_info -import org.meshtastic.core.resources.mqtt_probe_tcp_failure -import org.meshtastic.core.resources.mqtt_probe_timeout -import org.meshtastic.core.resources.mqtt_probe_tls_failure -import org.meshtastic.core.resources.mqtt_status_connected -import org.meshtastic.core.resources.mqtt_status_connecting -import org.meshtastic.core.resources.mqtt_status_disconnected -import org.meshtastic.core.resources.mqtt_status_disconnected_with_reason -import org.meshtastic.core.resources.mqtt_status_inactive -import org.meshtastic.core.resources.mqtt_status_reconnecting -import org.meshtastic.core.resources.mqtt_status_reconnecting_with_attempt -import org.meshtastic.core.resources.mqtt_test_connection import org.meshtastic.core.resources.password import org.meshtastic.core.resources.proxy_to_client_enabled import org.meshtastic.core.resources.root_topic @@ -91,8 +54,6 @@ import org.meshtastic.proto.ModuleConfig fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { val state by viewModel.radioConfigState.collectAsStateWithLifecycle() val destNode by viewModel.destNode.collectAsStateWithLifecycle() - val mqttProxyState by viewModel.mqttConnectionState.collectAsStateWithLifecycle() - val probeStatus by viewModel.mqttProbeStatus.collectAsStateWithLifecycle() val destNum = destNode?.num val mqttConfig = state.moduleConfig.mqtt ?: ModuleConfig.MQTTConfig() val formState = rememberConfigState(initialValue = mqttConfig) @@ -125,8 +86,6 @@ fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { viewModel.setModuleConfig(config) }, ) { - item { MqttStatusRow(mqttProxyState) } - item { TitledCard(title = stringResource(Res.string.mqtt_config)) { SwitchPreference( @@ -137,13 +96,16 @@ fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { containerColor = CardDefaults.cardColors().containerColor, ) HorizontalDivider() - MqttAddressAndProbe( + EditTextPreference( + title = stringResource(Res.string.address), + value = formState.value.address, + maxSize = 63, // address max_size:64 enabled = state.connected, - formState = formState, - probeStatus = probeStatus, - focusManager = focusManager, - onProbe = viewModel::probeMqttConnection, - onClearProbe = viewModel::clearMqttProbeStatus, + isError = false, + keyboardOptions = + KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), + onValueChanged = { formState.value = formState.value.copy(address = it) }, ) HorizontalDivider() EditTextPreference( @@ -248,129 +210,3 @@ fun MQTTConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { } private const val MIN_INTERVAL_SECS = 3600 - -private val AmberColor = Color(0xFFFFA000) -private val GreenColor = Color(0xFF4CAF50) - -@Composable -private fun MqttStatusRow(state: MqttConnectionState) { - val (label, color) = - when (state) { - is MqttConnectionState.Inactive -> - stringResource(Res.string.mqtt_status_inactive) to MaterialTheme.colorScheme.outline - is MqttConnectionState.Disconnected -> { - val text = - state.reason?.let { stringResource(Res.string.mqtt_status_disconnected_with_reason, it) } - ?: stringResource(Res.string.mqtt_status_disconnected) - text to MaterialTheme.colorScheme.error - } - is MqttConnectionState.Connecting -> stringResource(Res.string.mqtt_status_connecting) to AmberColor - is MqttConnectionState.Connected -> stringResource(Res.string.mqtt_status_connected) to GreenColor - is MqttConnectionState.Reconnecting -> { - val err = state.lastError - val text = - if (err != null) { - stringResource(Res.string.mqtt_status_reconnecting_with_attempt, state.attempt, err) - } else { - stringResource(Res.string.mqtt_status_reconnecting) - } - text to AmberColor - } - } - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.padding(horizontal = 4.dp), - ) { - Box(modifier = Modifier.size(10.dp).clip(CircleShape).background(color)) - Text( - text = label, - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } -} - -@Composable -private fun MqttAddressAndProbe( - enabled: Boolean, - formState: ConfigState, - probeStatus: MqttProbeStatus?, - focusManager: FocusManager, - onProbe: (address: String, tlsEnabled: Boolean, username: String, password: String) -> Unit, - onClearProbe: () -> Unit, -) { - EditTextPreference( - title = stringResource(Res.string.address), - value = formState.value.address, - maxSize = 63, // address max_size:64 - enabled = enabled, - isError = false, - keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { - formState.value = formState.value.copy(address = it) - onClearProbe() - }, - ) - HorizontalDivider() - MqttProbeRow( - enabled = enabled && formState.value.address.isNotBlank(), - status = probeStatus, - onTestClick = { - focusManager.clearFocus() - onProbe( - formState.value.address, - formState.value.tls_enabled, - formState.value.username, - formState.value.password, - ) - }, - ) -} - -@Composable -private fun MqttProbeRow(enabled: Boolean, status: MqttProbeStatus?, onTestClick: () -> Unit) { - Column( - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(4.dp), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - modifier = Modifier.fillMaxWidth(), - ) { - Button(onClick = onTestClick, enabled = enabled && status !is MqttProbeStatus.Probing) { - Text(stringResource(Res.string.mqtt_test_connection)) - } - val (probeText, probeColor) = status.toLabel() ?: return@Row - Text(text = probeText, style = MaterialTheme.typography.bodySmall, color = probeColor) - } - } -} - -@Composable -private fun MqttProbeStatus?.toLabel(): Pair? = when (this) { - null -> null - is MqttProbeStatus.Probing -> - stringResource(Res.string.mqtt_probe_running) to MaterialTheme.colorScheme.onSurfaceVariant - is MqttProbeStatus.Success -> { - val text = - serverInfo?.let { stringResource(Res.string.mqtt_probe_success_with_info, it) } - ?: stringResource(Res.string.mqtt_probe_success) - text to GreenColor - } - is MqttProbeStatus.Rejected -> - stringResource(Res.string.mqtt_probe_rejected, reason ?: reasonCode.toString()) to - MaterialTheme.colorScheme.error - is MqttProbeStatus.DnsFailure -> - stringResource(Res.string.mqtt_probe_dns_failure) to MaterialTheme.colorScheme.error - is MqttProbeStatus.TcpFailure -> - stringResource(Res.string.mqtt_probe_tcp_failure) to MaterialTheme.colorScheme.error - is MqttProbeStatus.TlsFailure -> - stringResource(Res.string.mqtt_probe_tls_failure) to MaterialTheme.colorScheme.error - is MqttProbeStatus.Timeout -> - stringResource(Res.string.mqtt_probe_timeout, timeoutMs.toInt()) to MaterialTheme.colorScheme.error - is MqttProbeStatus.Other -> - stringResource(Res.string.mqtt_probe_other_failure) to MaterialTheme.colorScheme.error -} diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PacketResponseStateDialog.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PacketResponseStateDialog.kt index c319c4f7f..18d79e08f 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PacketResponseStateDialog.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/PacketResponseStateDialog.kt @@ -35,7 +35,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay import org.jetbrains.compose.resources.stringResource -import org.meshtastic.core.common.util.MetricFormatter +import org.meshtastic.core.common.util.formatString import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.cancel import org.meshtastic.core.resources.close @@ -111,7 +111,7 @@ private fun LoadingContent(state: ResponseState.Loading, onComplete: () -> Unit) val progress by animateFloatAsState(targetValue = clampedProgress, label = "progress") Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( - text = MetricFormatter.percent(progress * 100f, decimalPlaces = 0), + text = formatString("%.0f%%", progress * 100f), style = MaterialTheme.typography.displaySmall, color = MaterialTheme.colorScheme.secondary, ) diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt index 526bd63ef..0e3c9058d 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/TAKConfigItemList.kt @@ -30,7 +30,6 @@ import org.meshtastic.core.model.getColorFrom import org.meshtastic.core.model.getStringResFrom import org.meshtastic.core.repository.TakPrefs import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.export_tak_data_package import org.meshtastic.core.resources.tak import org.meshtastic.core.resources.tak_config import org.meshtastic.core.resources.tak_role @@ -75,10 +74,7 @@ fun TAKConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) { onBack = onBack, actions = { IconButton(onClick = { exportLauncher("Meshtastic_TAK_Server.zip") }) { - Icon( - imageVector = MeshtasticIcons.Share, - contentDescription = stringResource(Res.string.export_tak_data_package), - ) + Icon(imageVector = MeshtasticIcons.Share, contentDescription = "Export TAK Data Package") } }, configState = formState, diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt index c1b7d8a9e..167daebbf 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt @@ -53,7 +53,6 @@ import org.meshtastic.core.repository.HomoglyphPrefs import org.meshtastic.core.repository.LocationRepository import org.meshtastic.core.repository.LocationService import org.meshtastic.core.repository.MapConsentPrefs -import org.meshtastic.core.repository.MqttManager import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.ServiceRepository @@ -100,7 +99,6 @@ class RadioConfigViewModelTest { private val processRadioResponseUseCase: ProcessRadioResponseUseCase = mock(MockMode.autofill) private val locationService: LocationService = mock(MockMode.autofill) private val fileService: FileService = mock(MockMode.autofill) - private val mqttManager: MqttManager = mock(MockMode.autofill) private val uiPrefs: UiPrefs = mock(MockMode.autofill) private lateinit var viewModel: RadioConfigViewModel @@ -123,9 +121,6 @@ class RadioConfigViewModelTest { every { serviceRepository.connectionState } returns MutableStateFlow(org.meshtastic.core.model.ConnectionState.Connected) - every { mqttManager.mqttConnectionState } returns - MutableStateFlow(org.meshtastic.core.model.MqttConnectionState.Inactive) - every { uiPrefs.showQuickChat } returns MutableStateFlow(false) viewModel = createViewModel() @@ -157,7 +152,6 @@ class RadioConfigViewModelTest { processRadioResponseUseCase = processRadioResponseUseCase, locationService = locationService, fileService = fileService, - mqttManager = mqttManager, ) @Test 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 a9a728559..5b63cc90a 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 ByteAr if (directory != null && file != null) { val targetFile = File(directory, file) val data = dataPackageProvider() - withContext(ioDispatcher) { targetFile.writeBytes(data) } + withContext(Dispatchers.IO) { targetFile.writeBytes(data) } Logger.i { "TAK data package exported successfully to ${targetFile.absolutePath}" } } } diff --git a/feature/widget/src/main/kotlin/org/meshtastic/feature/widget/AndroidAppWidgetUpdater.kt b/feature/widget/src/main/kotlin/org/meshtastic/feature/widget/AndroidAppWidgetUpdater.kt index c6cef8aa3..415e0e11d 100644 --- a/feature/widget/src/main/kotlin/org/meshtastic/feature/widget/AndroidAppWidgetUpdater.kt +++ b/feature/widget/src/main/kotlin/org/meshtastic/feature/widget/AndroidAppWidgetUpdater.kt @@ -17,48 +17,22 @@ package org.meshtastic.feature.widget import android.content.Context -import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.glance.appwidget.updateAll import co.touchlab.kermit.Logger -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.launch import org.koin.core.annotation.Single import org.meshtastic.core.repository.AppWidgetUpdater -private const val WIDGET_UPDATE_DEBOUNCE_MS = 500L - @Single -class AndroidAppWidgetUpdater(private val context: Context, stateProvider: LocalStatsWidgetStateProvider) : - AppWidgetUpdater { - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) - - init { - // Observe state changes and trigger a widget re-render whenever the data changes. - // Glance compositions are ephemeral — the widget cannot self-update via collectAsState() - // alone, so we must call updateAll() externally to drive re-renders. - @OptIn(FlowPreview::class) - scope.launch { - stateProvider.state - .debounce(WIDGET_UPDATE_DEBOUNCE_MS) - .distinctUntilChanged { old, new -> old.copy(updateTimeMillis = 0) == new.copy(updateTimeMillis = 0) } - .collect { if (hasWidgetInstances()) updateAll() } - } - } - - private suspend fun hasWidgetInstances(): Boolean = - GlanceAppWidgetManager(context).getGlanceIds(LocalStatsWidget::class.java).isNotEmpty() - +class AndroidAppWidgetUpdater(private val context: Context) : AppWidgetUpdater { override suspend fun updateAll() { + // Kickstart the widget composition. + // The widget internally uses collectAsState() and its own sampled StateFlow + // to drive updates automatically without excessive IPC and recreation. @Suppress("TooGenericExceptionCaught") try { LocalStatsWidget().updateAll(context) } catch (e: Exception) { - Logger.e(e) { "Failed to update widgets" } + co.touchlab.kermit.Logger.e(e) { "Failed to update widgets" } } } } diff --git a/feature/widget/src/main/kotlin/org/meshtastic/feature/widget/LocalStatsWidgetState.kt b/feature/widget/src/main/kotlin/org/meshtastic/feature/widget/LocalStatsWidgetState.kt index b8aca2664..ee40bd60b 100644 --- a/feature/widget/src/main/kotlin/org/meshtastic/feature/widget/LocalStatsWidgetState.kt +++ b/feature/widget/src/main/kotlin/org/meshtastic/feature/widget/LocalStatsWidgetState.kt @@ -76,6 +76,8 @@ data class LocalStatsWidgetUiState( val updateTimeMillis: Long = 0, ) +private const val WIDGET_SUBSCRIPTION_TIMEOUT_MS = 5_000L + @Single class LocalStatsWidgetStateProvider(nodeRepository: NodeRepository, serviceRepository: ServiceRepository) { private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) @@ -98,7 +100,12 @@ class LocalStatsWidgetStateProvider(nodeRepository: NodeRepository, serviceRepos .map { input -> mapToUiState(input.connectionState, input.totalNodes, input.onlineNodes, input.stats, input.localNode) } - .stateIn(scope = scope, started = SharingStarted.Eagerly, initialValue = LocalStatsWidgetUiState()) + .distinctUntilChanged() + .stateIn( + scope = scope, + started = SharingStarted.WhileSubscribed(WIDGET_SUBSCRIPTION_TIMEOUT_MS), + initialValue = LocalStatsWidgetUiState(), + ) private data class StateInput( val connectionState: ConnectionState, diff --git a/feature/widget/src/main/res/values/strings.xml b/feature/widget/src/main/res/values/strings.xml deleted file mode 100644 index 1e47c86ee..000000000 --- a/feature/widget/src/main/res/values/strings.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - Meshtastic - diff --git a/feature/widget/src/main/res/xml/widget_local_stats_info.xml b/feature/widget/src/main/res/xml/widget_local_stats_info.xml index 6dde1ea1e..da9863cd9 100644 --- a/feature/widget/src/main/res/xml/widget_local_stats_info.xml +++ b/feature/widget/src/main/res/xml/widget_local_stats_info.xml @@ -16,7 +16,6 @@ ~ along with this program. If not, see . -->