From a10fe61d0fd3dcd441188b6ef665e79a0fcc280b Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Tue, 17 Mar 2026 09:04:41 -0500 Subject: [PATCH 001/313] fix: resolve crashes and debug filter issues in Metrics and MapView (#4824) Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../kotlin/org/meshtastic/app/map/MapView.kt | 10 +++------- .../org/meshtastic/app/map/MapViewModel.kt | 5 +---- .../app/navigation/NodesNavigation.kt | 9 ++++++--- .../meshtastic/app/di/KoinVerificationTest.kt | 7 ++++++- .../feature/map/BaseMapViewModel.kt | 2 +- .../settings/debugging/DebugViewModel.kt | 20 +++++++++---------- 6 files changed, 27 insertions(+), 26 deletions(-) diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt index 1ba1e02f7..afbedfa0b 100644 --- a/app/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt @@ -28,7 +28,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.rememberScrollState @@ -97,7 +96,6 @@ import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.common.util.nowSeconds import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.Node -import org.meshtastic.core.model.util.toString import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.calculating import org.meshtastic.core.resources.cancel @@ -107,10 +105,7 @@ import org.meshtastic.core.resources.delete_for_everyone import org.meshtastic.core.resources.delete_for_me import org.meshtastic.core.resources.expires import org.meshtastic.core.resources.getString -import org.meshtastic.core.resources.heading -import org.meshtastic.core.resources.latitude import org.meshtastic.core.resources.location_disabled -import org.meshtastic.core.resources.longitude import org.meshtastic.core.resources.map_cache_info import org.meshtastic.core.resources.map_cache_manager import org.meshtastic.core.resources.map_cache_size @@ -142,7 +137,6 @@ import org.meshtastic.core.ui.util.showToast import org.meshtastic.feature.map.LastHeardFilter import org.meshtastic.feature.map.model.TracerouteOverlay import org.meshtastic.feature.map.tracerouteNodeSelection -import org.meshtastic.proto.Config.DisplayConfig.DisplayUnits import org.meshtastic.proto.Position import org.meshtastic.proto.Waypoint import org.osmdroid.bonuspack.utils.BonusPackHelper.getBitmapFromVectorDrawable @@ -444,7 +438,9 @@ fun MapView( if (node.batteryStr != "") node.batteryStr else "?", ) ourNode?.distanceStr(node, displayUnits)?.let { dist -> - subDescription = getString(Res.string.map_subDescription, ourNode.bearing(node).toString(), dist) + ourNode.bearing(node)?.let { bearing -> + subDescription = getString(Res.string.map_subDescription, bearing, dist) + } } setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM) position = nodePosition diff --git a/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt b/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt index ab891cbc6..4bb2c9083 100644 --- a/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt +++ b/app/src/fdroid/kotlin/org/meshtastic/app/map/MapViewModel.kt @@ -22,7 +22,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import org.koin.core.annotation.KoinViewModel import org.meshtastic.core.common.BuildConfigProvider -import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.RadioController import org.meshtastic.core.repository.MapPrefs import org.meshtastic.core.repository.NodeRepository @@ -37,7 +36,7 @@ import org.meshtastic.proto.LocalConfig class MapViewModel( mapPrefs: MapPrefs, packetRepository: PacketRepository, - override val nodeRepository: NodeRepository, + nodeRepository: NodeRepository, radioController: RadioController, radioConfigRepository: RadioConfigRepository, buildConfigProvider: BuildConfigProvider, @@ -65,6 +64,4 @@ class MapViewModel( get() = localConfig.value val applicationId = buildConfigProvider.applicationId - - override fun getUser(userId: String?) = nodeRepository.getUser(userId ?: DataPacket.ID_BROADCAST) } diff --git a/app/src/main/kotlin/org/meshtastic/app/navigation/NodesNavigation.kt b/app/src/main/kotlin/org/meshtastic/app/navigation/NodesNavigation.kt index 24893c7a7..34c742882 100644 --- a/app/src/main/kotlin/org/meshtastic/app/navigation/NodesNavigation.kt +++ b/app/src/main/kotlin/org/meshtastic/app/navigation/NodesNavigation.kt @@ -34,6 +34,7 @@ import androidx.navigation3.runtime.NavKey import kotlinx.coroutines.flow.Flow import org.jetbrains.compose.resources.StringResource import org.koin.compose.viewmodel.koinViewModel +import org.koin.core.parameter.parametersOf import org.meshtastic.app.map.node.NodeMapScreen import org.meshtastic.app.ui.node.AdaptiveNodeListScreen import org.meshtastic.core.navigation.ContactsRoutes @@ -115,7 +116,8 @@ fun EntryProviderScope.nodeDetailGraph( } entry { args -> - val metricsViewModel = koinViewModel() + val metricsViewModel = + koinViewModel(key = "metrics-${args.destNum}") { parametersOf(args.destNum) } metricsViewModel.setNodeId(args.destNum) TracerouteLogScreen( @@ -134,7 +136,8 @@ fun EntryProviderScope.nodeDetailGraph( } entry { args -> - val metricsViewModel = koinViewModel() + val metricsViewModel = + koinViewModel(key = "metrics-${args.destNum}") { parametersOf(args.destNum) } metricsViewModel.setNodeId(args.destNum) TracerouteMapScreen( @@ -176,8 +179,8 @@ private inline fun EntryProviderScope.addNodeDetailS crossinline getDestNum: (R) -> Int, ) { entry { args -> - val metricsViewModel = koinViewModel() val destNum = getDestNum(args) + val metricsViewModel = koinViewModel(key = "metrics-$destNum") { parametersOf(destNum) } metricsViewModel.setNodeId(destNum) routeInfo.screenComposable(metricsViewModel) { backStack.removeLastOrNull() } diff --git a/app/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt b/app/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt index 341d25ccf..d71c7dd9c 100644 --- a/app/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt +++ b/app/src/test/kotlin/org/meshtastic/app/di/KoinVerificationTest.kt @@ -32,6 +32,7 @@ import org.koin.test.verify.injectedParameters import org.koin.test.verify.verify import org.meshtastic.app.map.MapViewModel import org.meshtastic.core.model.util.NodeIdLookup +import org.meshtastic.feature.node.metrics.MetricsViewModel class KoinVerificationTest { @@ -54,7 +55,11 @@ class KoinVerificationTest { HttpClientEngine::class, OkHttpClient::class, ), - injections = injectedParameters(definition(SavedStateHandle::class)), + injections = + injectedParameters( + definition(SavedStateHandle::class), + definition(Int::class), + ), ) } } diff --git a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt index a7caf78a9..73dcbe499 100644 --- a/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt +++ b/feature/map/src/commonMain/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt @@ -47,7 +47,7 @@ import org.meshtastic.proto.Waypoint @Suppress("TooManyFunctions") open class BaseMapViewModel( protected val mapPrefs: MapPrefs, - protected open val nodeRepository: NodeRepository, + protected val nodeRepository: NodeRepository, private val packetRepository: PacketRepository, private val radioController: RadioController, ) : ViewModel() { diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt index bca6235b7..c3410f33d 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt @@ -193,19 +193,19 @@ class LogFilterManager { return logs.filter { logItem -> when (filterMode) { FilterMode.OR -> - filterTexts.any { - it.contains(logItem.logMessage, ignoreCase = true) || - it.contains(logItem.messageType, ignoreCase = true) || - it.contains(logItem.formattedReceivedDate, ignoreCase = true) || - (logItem.decodedPayload?.contains(it, ignoreCase = true) == true) + filterTexts.any { filter -> + logItem.logMessage.contains(filter, ignoreCase = true) || + logItem.messageType.contains(filter, ignoreCase = true) || + logItem.formattedReceivedDate.contains(filter, ignoreCase = true) || + (logItem.decodedPayload?.contains(filter, ignoreCase = true) == true) } FilterMode.AND -> - filterTexts.all { - it.contains(logItem.logMessage, ignoreCase = true) || - it.contains(logItem.messageType, ignoreCase = true) || - it.contains(logItem.formattedReceivedDate, ignoreCase = true) || - (logItem.decodedPayload?.contains(it, ignoreCase = true) == true) + filterTexts.all { filter -> + logItem.logMessage.contains(filter, ignoreCase = true) || + logItem.messageType.contains(filter, ignoreCase = true) || + logItem.formattedReceivedDate.contains(filter, ignoreCase = true) || + (logItem.decodedPayload?.contains(filter, ignoreCase = true) == true) } } } From 212acaecacc5ff4b42cb1695934a7130d532bc39 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 09:25:30 -0500 Subject: [PATCH 002/313] chore(deps): update core/proto/src/main/proto digest to bc8e638 (#4823) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- core/proto/src/main/proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/proto/src/main/proto b/core/proto/src/main/proto index cdde2876b..bc8e63833 160000 --- a/core/proto/src/main/proto +++ b/core/proto/src/main/proto @@ -1 +1 @@ -Subproject commit cdde2876befc50620307497e269f313c7944fc0b +Subproject commit bc8e63833afda986bd0635a3879890df1d652ae8 From 5eb6e501c03c8df4ccd0b83070b43cfcb526fd08 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Tue, 17 Mar 2026 09:25:38 -0500 Subject: [PATCH 003/313] chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4822) --- app/src/main/assets/firmware_releases.json | 6 ------ .../composeResources/values-it/strings.xml | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/app/src/main/assets/firmware_releases.json b/app/src/main/assets/firmware_releases.json index efc14c593..6e1d9c702 100644 --- a/app/src/main/assets/firmware_releases.json +++ b/app/src/main/assets/firmware_releases.json @@ -217,12 +217,6 @@ "title": "Add PiMesh-1W V1/V2 Portduino LoRa config files", "page_url": "https://github.com/meshtastic/firmware/pull/9857", "zip_url": "https://discord.com/invite/meshtastic" - }, - { - "id": "9827", - "title": "Align 920–925 MHz limits as per NBTC regulations in Thailand (27 dBm, 10% duty cycle) ", - "page_url": "https://github.com/meshtastic/firmware/pull/9827", - "zip_url": "https://discord.com/invite/meshtastic" } ] } \ No newline at end of file diff --git a/core/resources/src/commonMain/composeResources/values-it/strings.xml b/core/resources/src/commonMain/composeResources/values-it/strings.xml index 70c22817e..f21b3873d 100644 --- a/core/resources/src/commonMain/composeResources/values-it/strings.xml +++ b/core/resources/src/commonMain/composeResources/values-it/strings.xml @@ -39,11 +39,14 @@ via MQTT via UDP via API + Interno via Preferiti Visualizza solo i nodi ignorati Non riconosciuto In attesa di conferma In coda per l'invio + Percorso tramite catena SF++… + Confermato sulla catena SF++ Confermato Nessun percorso Ricevuta una conferma negativa @@ -65,6 +68,7 @@ App collegata o dispositivo di messaggistica standalone. Client Mute Dispositivo che non inoltra pacchetti da altri dispositivi. + Base Client Tratta i pacchetti da o verso i nodi preferiti come ROUTER_LATE, e tutti gli altri pacchetti come CLIENT. Router Nodo d'infrastruttura per estendere la copertura di rete tramite inoltro dei messaggi. Visibile nell'elenco dei nodi. @@ -117,6 +121,16 @@ Le preimpostazioni del modem disponibili, la predefinita è Long Fast. Imposta il numero massimo di hop, il predefinito è 3. Aumentare gli hop comporta anche aumentare la congestione e dovrebbe essere utilizzato con attenzione. Con 0 hop, i messaggi non otterranno conferma di ricezione. La frequenza di funzionamento del nodo viene calcolata in base alla regione, alla preimpostazione del modem e a questo campo. Quando è a 0, lo slot viene calcolato automaticamente in base al nome del canale primario e cambierà rispetto allo slot pubblico predefinito. Torna allo slot pubblico predefinito se sono configurati canali primari privati e secondari pubblici. + Distanza Molto Grande / Lento + Distanza Grande / Lento + Lungo Raggio - Turbo + Lungo Raggio - Moderato + Distanza Molto Grande / Lento + Distanza Media / Lento + Distanza Media / Lento + Lungo Raggio - Turbo + Distanza Breve / Veloce + Distanza Breve / Lento L'attivazione della WiFi disabiliterà la connessione bluetooth con l'app. L'attivazione della connessione Ethernet disabiliterà la connessione bluetooth all'app. La connessione al nodo via TCP non è disponibile per i dispositivi Apple. Abilita la trasmissione di pacchetti tramite UDP sulla rete locale. @@ -358,6 +372,7 @@ Formato codice QR delle Credenziali WiFi non valido Torna Indietro Batteria + Canale di utilizzo Temperatura Umidità Temperatura Del Suolo @@ -539,6 +554,7 @@ Doppio tocco come pressione pulsante Triple Click Ad Hoc Ping Fuso Orario + Battito Cuore Led Schermo Dispositivo Tieni lo schermo acceso per Durata di ogni schermata @@ -950,6 +966,7 @@ Configurazione dispositivo "[Remote] %1$s" Invia Telemetria Dispositivo + Abilita/Disabilita Il dispositivo modulo per la telemetria nella rete mesh Qualsiasi 1 Ora 8 Ore From 190e62ce687a2bde7f6cd534500e0193c6bb4ef2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:07:18 -0500 Subject: [PATCH 004/313] chore(deps): update datadog to v1.24.0 (#4826) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a1f8193f9..9c75bb8c8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -235,7 +235,7 @@ android-tools-common = { module = "com.android.tools:common", version = "32.1.0" androidx-room-gradlePlugin = { module = "androidx.room:room-gradle-plugin", version.ref = "room" } compose-gradlePlugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" } compose-multiplatform-gradlePlugin = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "compose-multiplatform" } -datadog-gradlePlugin = { module = "com.datadoghq.dd-sdk-android-gradle-plugin:com.datadoghq.dd-sdk-android-gradle-plugin.gradle.plugin", version = "1.23.0" } +datadog-gradlePlugin = { module = "com.datadoghq.dd-sdk-android-gradle-plugin:com.datadoghq.dd-sdk-android-gradle-plugin.gradle.plugin", version = "1.24.0" } detekt-compose = { module = "io.nlopez.compose.rules:detekt", version = "0.5.6" } detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } detekt-gradlePlugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } @@ -276,7 +276,7 @@ firebase-perf = { id = "com.google.firebase.firebase-perf", version = "2.0.2" } # Other aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlibraries" } -datadog = { id = "com.datadoghq.dd-sdk-android-gradle-plugin", version = "1.23.0" } +datadog = { id = "com.datadoghq.dd-sdk-android-gradle-plugin", version = "1.24.0" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } wire = { id = "com.squareup.wire", version.ref = "wire" } From 0c3a841a807a4c2cd8184ca7b32e94a7cb855c6f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:07:36 -0500 Subject: [PATCH 005/313] chore(deps): update koin to v4.2.0 (#4827) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9c75bb8c8..186e3b869 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ navigation3 = "1.1.0-alpha04" paging = "3.4.2" room = "2.8.4" savedstate = "1.4.0" -koin = "4.2.0-RC2" +koin = "4.2.0" koin-annotations = "2.1.0" koin-plugin = "0.4.0" From 0d0bdf9172a7f1d4747b40f9f8ee47e2c8bccc80 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:05:21 -0500 Subject: [PATCH 006/313] chore(deps): update core/proto/src/main/proto digest to eba2d94 (#4830) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- core/proto/src/main/proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/proto/src/main/proto b/core/proto/src/main/proto index bc8e63833..eba2d94c8 160000 --- a/core/proto/src/main/proto +++ b/core/proto/src/main/proto @@ -1 +1 @@ -Subproject commit bc8e63833afda986bd0635a3879890df1d652ae8 +Subproject commit eba2d94c8d53e798f560e12d63d0457e1e22759e From 807db83f53491298c4edfeb99294b3d4f3d1c84c Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:06:01 -0500 Subject: [PATCH 007/313] feat: service extraction (#4828) --- app/src/main/AndroidManifest.xml | 10 +- .../org/meshtastic/app/MeshServiceClient.kt | 4 +- .../org/meshtastic/app/MeshUtilApplication.kt | 2 +- .../org/meshtastic/app/di/AppKoinModule.kt | 4 +- .../domain/worker/WorkManagerMessageQueue.kt | 1 + .../main/kotlin/org/meshtastic/app/ui/Main.kt | 2 +- .../extract_services_20260317/index.md | 5 + .../extract_services_20260317/metadata.json | 8 ++ .../archive/extract_services_20260317/plan.md | 44 +++++++ .../archive/extract_services_20260317/spec.md | 32 +++++ conductor/product.md | 2 +- conductor/tech-stack.md | 3 + conductor/tracks.md | 2 - .../core/data/manager/CommandSenderImpl.kt | 6 +- .../meshtastic/core/model/DeviceVersion.kt | 5 + core/network/build.gradle.kts | 3 + .../radio/AndroidRadioInterfaceService.kt | 9 +- .../core/network}/radio/BleRadioInterface.kt | 4 +- .../radio/BleRadioInterfaceFactory.kt | 2 +- .../network}/radio/BleRadioInterfaceSpec.kt | 2 +- .../core/network}/radio/InterfaceFactory.kt | 2 +- .../core/network}/radio/SerialInterface.kt | 8 +- .../network}/radio/SerialInterfaceFactory.kt | 4 +- .../network}/radio/SerialInterfaceSpec.kt | 4 +- .../core/network}/radio/TCPInterface.kt | 2 +- .../network}/radio/TCPInterfaceFactory.kt | 2 +- .../core/network}/radio/TCPInterfaceSpec.kt | 2 +- .../repository/ConnectivityManager.kt | 2 +- .../network}/repository/NetworkRepository.kt | 2 +- .../core/network}/repository/NsdManager.kt | 4 +- .../network}/repository/ProbeTableProvider.kt | 4 +- .../network}/repository/SerialConnection.kt | 2 +- .../repository/SerialConnectionImpl.kt | 4 +- .../repository/SerialConnectionListener.kt | 2 +- .../repository/UsbBroadcastReceiver.kt | 2 +- .../core/network}/repository/UsbManager.kt | 2 +- .../core/network}/repository/UsbRepository.kt | 2 +- .../network/radio/BleRadioInterfaceTest.kt | 2 +- .../network}/radio/InterfaceFactorySpi.kt | 2 +- .../core/network}/radio/InterfaceSpec.kt | 2 +- .../core/network}/radio/MockInterface.kt | 2 +- .../network}/radio/MockInterfaceFactory.kt | 2 +- .../core/network}/radio/MockInterfaceSpec.kt | 2 +- .../core/network}/radio/NopInterface.kt | 2 +- .../network}/radio/NopInterfaceFactory.kt | 2 +- .../core/network}/radio/NopInterfaceSpec.kt | 2 +- .../core/network}/radio/StreamInterface.kt | 2 +- .../network}/repository/NetworkConstants.kt | 2 +- .../src/androidMain}/res/raw/alert.mp3 | Bin core/service/build.gradle.kts | 1 + .../core}/service/AndroidMeshWorkerManager.kt | 4 +- .../service/AndroidRadioControllerImpl.kt | 2 +- .../core}/service/BootCompleteReceiver.kt | 2 +- .../org/meshtastic/core}/service/Constants.kt | 2 +- .../core}/service/MarkAsReadReceiver.kt | 2 +- .../meshtastic/core}/service/MeshService.kt | 42 ++----- .../service/MeshServiceNotificationsImpl.kt | 26 +++-- .../core}/service/MeshServiceStarter.kt | 7 +- .../core}/service/ReactionReceiver.kt | 2 +- .../meshtastic/core}/service/ReplyReceiver.kt | 2 +- .../core}/service/ServiceBroadcasts.kt | 2 +- .../service}/worker/MeshLogCleanupWorker.kt | 2 +- .../core/service}/worker/SendMessageWorker.kt | 2 +- .../service}/worker/ServiceKeepAliveWorker.kt | 9 +- .../core/service/MeshServiceOrchestrator.kt | 2 + .../service/MeshServiceOrchestratorTest.kt | 77 ++++++++++++ .../kotlin/org/meshtastic/desktop/Main.kt | 4 +- .../desktop/di/DesktopKoinModule.kt | 14 --- .../radio/DesktopMeshServiceController.kt | 110 ------------------ docs/kmp-status.md | 14 ++- docs/roadmap.md | 3 +- feature/connections/build.gradle.kts | 1 + .../connections/AndroidScannerViewModel.kt | 2 +- .../AndroidGetDiscoveredDevicesUseCase.kt | 6 +- .../ui/components/NetworkDevices.kt | 2 +- .../meshserviceexample/MainActivity.kt | 2 +- 76 files changed, 309 insertions(+), 257 deletions(-) create mode 100644 conductor/archive/extract_services_20260317/index.md create mode 100644 conductor/archive/extract_services_20260317/metadata.json create mode 100644 conductor/archive/extract_services_20260317/plan.md create mode 100644 conductor/archive/extract_services_20260317/spec.md rename {app/src/main/kotlin/org/meshtastic/app/repository => core/network/src/androidMain/kotlin/org/meshtastic/core/network}/radio/AndroidRadioInterfaceService.kt (97%) rename {app/src/main/kotlin/org/meshtastic/app/repository => core/network/src/androidMain/kotlin/org/meshtastic/core/network}/radio/BleRadioInterface.kt (99%) rename {app/src/main/kotlin/org/meshtastic/app/repository => core/network/src/androidMain/kotlin/org/meshtastic/core/network}/radio/BleRadioInterfaceFactory.kt (97%) rename {app/src/main/kotlin/org/meshtastic/app/repository => core/network/src/androidMain/kotlin/org/meshtastic/core/network}/radio/BleRadioInterfaceSpec.kt (97%) rename {app/src/main/kotlin/org/meshtastic/app/repository => core/network/src/androidMain/kotlin/org/meshtastic/core/network}/radio/InterfaceFactory.kt (98%) rename {app/src/main/kotlin/org/meshtastic/app/repository => core/network/src/androidMain/kotlin/org/meshtastic/core/network}/radio/SerialInterface.kt (95%) rename {app/src/main/kotlin/org/meshtastic/app/repository => core/network/src/androidMain/kotlin/org/meshtastic/core/network}/radio/SerialInterfaceFactory.kt (90%) rename {app/src/main/kotlin/org/meshtastic/app/repository => core/network/src/androidMain/kotlin/org/meshtastic/core/network}/radio/SerialInterfaceSpec.kt (94%) rename {app/src/main/kotlin/org/meshtastic/app/repository => core/network/src/androidMain/kotlin/org/meshtastic/core/network}/radio/TCPInterface.kt (98%) rename {app/src/main/kotlin/org/meshtastic/app/repository => core/network/src/androidMain/kotlin/org/meshtastic/core/network}/radio/TCPInterfaceFactory.kt (96%) rename {app/src/main/kotlin/org/meshtastic/app/repository => core/network/src/androidMain/kotlin/org/meshtastic/core/network}/radio/TCPInterfaceSpec.kt (96%) rename {feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections => core/network/src/androidMain/kotlin/org/meshtastic/core/network}/repository/ConnectivityManager.kt (97%) rename {feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections => core/network/src/androidMain/kotlin/org/meshtastic/core/network}/repository/NetworkRepository.kt (98%) rename {feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections => core/network/src/androidMain/kotlin/org/meshtastic/core/network}/repository/NsdManager.kt (98%) rename {feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections => core/network/src/androidMain/kotlin/org/meshtastic/core/network}/repository/ProbeTableProvider.kt (94%) rename {feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections => core/network/src/androidMain/kotlin/org/meshtastic/core/network}/repository/SerialConnection.kt (95%) rename {feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections => core/network/src/androidMain/kotlin/org/meshtastic/core/network}/repository/SerialConnectionImpl.kt (98%) rename {feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections => core/network/src/androidMain/kotlin/org/meshtastic/core/network}/repository/SerialConnectionListener.kt (95%) rename {feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections => core/network/src/androidMain/kotlin/org/meshtastic/core/network}/repository/UsbBroadcastReceiver.kt (97%) rename {feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections => core/network/src/androidMain/kotlin/org/meshtastic/core/network}/repository/UsbManager.kt (97%) rename {feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections => core/network/src/androidMain/kotlin/org/meshtastic/core/network}/repository/UsbRepository.kt (98%) rename {app/src/main/kotlin/org/meshtastic/app/repository => core/network/src/commonMain/kotlin/org/meshtastic/core/network}/radio/InterfaceFactorySpi.kt (96%) rename {app/src/main/kotlin/org/meshtastic/app/repository => core/network/src/commonMain/kotlin/org/meshtastic/core/network}/radio/InterfaceSpec.kt (96%) rename {app/src/main/kotlin/org/meshtastic/app/repository => core/network/src/commonMain/kotlin/org/meshtastic/core/network}/radio/MockInterface.kt (99%) rename {app/src/main/kotlin/org/meshtastic/app/repository => core/network/src/commonMain/kotlin/org/meshtastic/core/network}/radio/MockInterfaceFactory.kt (95%) rename {app/src/main/kotlin/org/meshtastic/app/repository => core/network/src/commonMain/kotlin/org/meshtastic/core/network}/radio/MockInterfaceSpec.kt (96%) rename {app/src/main/kotlin/org/meshtastic/app/repository => core/network/src/commonMain/kotlin/org/meshtastic/core/network}/radio/NopInterface.kt (95%) rename {app/src/main/kotlin/org/meshtastic/app/repository => core/network/src/commonMain/kotlin/org/meshtastic/core/network}/radio/NopInterfaceFactory.kt (95%) rename {app/src/main/kotlin/org/meshtastic/app/repository => core/network/src/commonMain/kotlin/org/meshtastic/core/network}/radio/NopInterfaceSpec.kt (96%) rename {app/src/main/kotlin/org/meshtastic/app/repository => core/network/src/commonMain/kotlin/org/meshtastic/core/network}/radio/StreamInterface.kt (98%) rename {feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections => core/network/src/commonMain/kotlin/org/meshtastic/core/network}/repository/NetworkConstants.kt (93%) rename {app/src/main => core/resources/src/androidMain}/res/raw/alert.mp3 (100%) rename {app/src/main/kotlin/org/meshtastic/app => core/service/src/androidMain/kotlin/org/meshtastic/core}/service/AndroidMeshWorkerManager.kt (93%) rename {app/src/main/kotlin/org/meshtastic/app => core/service/src/androidMain/kotlin/org/meshtastic/core}/service/BootCompleteReceiver.kt (97%) rename {app/src/main/kotlin/org/meshtastic/app => core/service/src/androidMain/kotlin/org/meshtastic/core}/service/Constants.kt (98%) rename {app/src/main/kotlin/org/meshtastic/app => core/service/src/androidMain/kotlin/org/meshtastic/core}/service/MarkAsReadReceiver.kt (98%) rename {app/src/main/kotlin/org/meshtastic/app => core/service/src/androidMain/kotlin/org/meshtastic/core}/service/MeshService.kt (90%) rename {app/src/main/kotlin/org/meshtastic/app => core/service/src/androidMain/kotlin/org/meshtastic/core}/service/MeshServiceNotificationsImpl.kt (97%) rename {app/src/main/kotlin/org/meshtastic/app => core/service/src/androidMain/kotlin/org/meshtastic/core}/service/MeshServiceStarter.kt (92%) rename {app/src/main/kotlin/org/meshtastic/app => core/service/src/androidMain/kotlin/org/meshtastic/core}/service/ReactionReceiver.kt (98%) rename {app/src/main/kotlin/org/meshtastic/app => core/service/src/androidMain/kotlin/org/meshtastic/core}/service/ReplyReceiver.kt (98%) rename {app/src/main/kotlin/org/meshtastic/app => core/service/src/androidMain/kotlin/org/meshtastic/core}/service/ServiceBroadcasts.kt (99%) rename {app/src/main/kotlin/org/meshtastic/app => core/service/src/androidMain/kotlin/org/meshtastic/core/service}/worker/MeshLogCleanupWorker.kt (98%) rename {app/src/main/kotlin/org/meshtastic/app/messaging/domain => core/service/src/androidMain/kotlin/org/meshtastic/core/service}/worker/SendMessageWorker.kt (97%) rename {app/src/main/kotlin/org/meshtastic/app => core/service/src/androidMain/kotlin/org/meshtastic/core/service}/worker/ServiceKeepAliveWorker.kt (93%) create mode 100644 core/service/src/commonTest/kotlin/org/meshtastic/core/service/MeshServiceOrchestratorTest.kt delete mode 100644 desktop/src/main/kotlin/org/meshtastic/desktop/radio/DesktopMeshServiceController.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a19b6ff3c..7828802d9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -152,7 +152,7 @@ @@ -228,7 +228,7 @@ android:resource="@xml/device_filter" /> - @@ -252,9 +252,9 @@ android:path="com.geeksville.mesh" /> --> - - - + + + 80%) for all extracted and refactored code [9cff9bc] +- [x] Task: Remove any lingering unused dependencies or dead code in `app` [e39d2e2] +- [x] Task: Conductor - User Manual Verification 'Verification & Cleanup' (Protocol in workflow.md) + +## Phase: Review Fixes +- [x] Task: Apply review suggestions [1ae9fb6] \ No newline at end of file diff --git a/conductor/archive/extract_services_20260317/spec.md b/conductor/archive/extract_services_20260317/spec.md new file mode 100644 index 000000000..32d1eb803 --- /dev/null +++ b/conductor/archive/extract_services_20260317/spec.md @@ -0,0 +1,32 @@ +# Specification: Extract service/worker/radio files from `app` + +## Overview +This track aims to decouple the main `app` module by extracting Android-specific service, WorkManager worker, and radio connection files into `core:service` and `core:network` modules. The goal is to maximize code reuse across Kotlin Multiplatform (KMP) targets, clarify class responsibilities, and improve unit testability by isolating the network and service layers. + +## Goals +- **Decouple `app`:** Remove Android-specific service dependencies from the main app module. +- **KMP Preparation:** Migrate as much logic as possible into `commonMain` for reuse across platforms. +- **Desktop Integration:** If logic is successfully abstracted into `commonMain`, integrate and use it within the `desktop` target to ensure reusability. +- **Testability:** Isolate service and network layers to facilitate better unit testing. +- **Simplification:** Refactor logic during the move to clarify and simplify responsibilities. + +## Functional Requirements +- Identify all service, worker, and radio-related classes currently residing in the `app` module. +- Move Android-specific implementations (e.g., `Service`, `Worker`) to `core:service/androidMain` and `core:network/androidMain`. +- Extract platform-agnostic business logic and interfaces into `commonMain` within those core modules. +- Refactor existing logic where necessary to establish a clear delineation of responsibility. +- Update all dependency injections (Koin modules) and imports across the project to reflect the new locations. +- Attempt to wire up the newly abstracted shared logic within the `desktop` module if applicable. + +## Non-Functional Requirements +- **Architecture Compliance:** Changes must adhere to the MVI / Unidirectional Data Flow and KMP structures defined in `tech-stack.md`. +- **Performance:** Refactoring should not negatively impact app startup time or background processing efficiency. +- **Code Coverage:** Maintain or improve overall test coverage for the extracted components (>80% target). + +## Acceptance Criteria +- [ ] No service, worker, or radio connection classes remain in the `app` module. +- [ ] Extracted Android-specific classes compile successfully in `core:service/androidMain` and `core:network/androidMain`. +- [ ] Shared business logic compiles successfully in `core:service/commonMain` and `core:network/commonMain`. +- [ ] If logic is abstracted for reuse, it is integrated and utilized in the `desktop` target where applicable. +- [ ] The app compiles, installs, and runs without regressions in background processing or radio connectivity. +- [ ] Unit tests for the moved and refactored classes pass. \ No newline at end of file diff --git a/conductor/product.md b/conductor/product.md index 53a1d4dc2..ccbd0a648 100644 --- a/conductor/product.md +++ b/conductor/product.md @@ -20,6 +20,6 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application designed to facil - Device configuration and firmware updates ## Key Architecture Goals -- Provide a robust, shared KMP core (`core:model`, `core:ble`, `core:repository`, `core:domain`, `core:data`, `core:network`) to support multiple platforms (Android, Desktop, iOS) +- Provide a robust, shared KMP core (`core:model`, `core:ble`, `core:repository`, `core:domain`, `core:data`, `core:network`, `core:service`) to support multiple platforms (Android, Desktop, iOS) - Ensure offline-first functionality and resilient data persistence (Room KMP) - Decouple UI logic into shared components (`core:ui`, `feature:*`) using Compose Multiplatform \ No newline at end of file diff --git a/conductor/tech-stack.md b/conductor/tech-stack.md index a9b6331f8..c6ea7ebbd 100644 --- a/conductor/tech-stack.md +++ b/conductor/tech-stack.md @@ -7,6 +7,9 @@ - **Compose Multiplatform:** Shared UI layer for rendering on Android and Desktop. - **Jetpack Compose:** Used where platform-specific UI (like charts or permissions) is necessary on Android. +## Background & Services +- **Platform Services:** Core service orchestrations and background work are abstracted into `core:service` to maximize logic reuse across targets, using platform-specific implementations (e.g., WorkManager/Service on Android) only where necessary. + ## Architecture - **MVI / Unidirectional Data Flow:** Shared view models using the multiplatform `androidx.lifecycle.ViewModel`. - **JetBrains Navigation 3:** Multiplatform fork for state-based, compose-first navigation without relying on `NavController`. diff --git a/conductor/tracks.md b/conductor/tracks.md index 0b5c54e3d..22d3d6494 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -1,5 +1,3 @@ # Project Tracks This file tracks all major tracks for the project. Each track has its own detailed plan in its respective folder. - ---- diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt index b296cef01..1e5f5eaeb 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt @@ -258,7 +258,7 @@ class CommandSenderImpl( wantAck = true, id = requestId, channel = nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0, - decoded = Data(portnum = PortNum.TRACEROUTE_APP, want_response = true), + decoded = Data(portnum = PortNum.TRACEROUTE_APP, want_response = true, dest = destNum), ), ) } @@ -296,7 +296,7 @@ class CommandSenderImpl( to = destNum, id = requestId, channel = nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0, - decoded = Data(portnum = portNum, payload = payloadBytes, want_response = true), + decoded = Data(portnum = portNum, payload = payloadBytes, want_response = true, dest = destNum), ), ) } @@ -349,7 +349,7 @@ class CommandSenderImpl( wantAck = true, id = requestId, channel = nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0, - decoded = Data(portnum = PortNum.NEIGHBORINFO_APP, want_response = true), + decoded = Data(portnum = PortNum.NEIGHBORINFO_APP, want_response = true, dest = destNum), ), ) } diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DeviceVersion.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DeviceVersion.kt index d72d7775f..4816e9eb3 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DeviceVersion.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/DeviceVersion.kt @@ -52,4 +52,9 @@ data class DeviceVersion(val asString: String) : Comparable { } override fun compareTo(other: DeviceVersion): Int = asInt.compareTo(other.asInt) + + companion object { + const val MIN_FW_VERSION = "2.5.14" + const val ABS_MIN_FW_VERSION = "2.3.15" + } } diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index 06ac5016b..4fd91682f 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -51,7 +51,10 @@ kotlin { val jvmMain by getting { dependencies { implementation(libs.ktor.client.java) } } androidMain.dependencies { + implementation(projects.core.ble) + implementation(projects.core.prefs) implementation(libs.org.eclipse.paho.client.mqttv3) + implementation(libs.usb.serial.android) implementation(libs.coil.network.okhttp) implementation(libs.coil.svg) implementation(libs.ktor.client.okhttp) diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/AndroidRadioInterfaceService.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/AndroidRadioInterfaceService.kt similarity index 97% rename from app/src/main/kotlin/org/meshtastic/app/repository/radio/AndroidRadioInterfaceService.kt rename to core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/AndroidRadioInterfaceService.kt index 88d739fe0..c90ae08d0 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/AndroidRadioInterfaceService.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/AndroidRadioInterfaceService.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.repository.radio +package org.meshtastic.core.network.radio import android.app.Application import android.provider.Settings @@ -37,8 +37,8 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.koin.core.annotation.Named import org.koin.core.annotation.Single -import org.meshtastic.app.BuildConfig import org.meshtastic.core.ble.BluetoothRepository +import org.meshtastic.core.common.BuildConfigProvider import org.meshtastic.core.common.util.BinaryLogFile import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.common.util.ignoreException @@ -49,11 +49,11 @@ import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.InterfaceId import org.meshtastic.core.model.MeshActivity import org.meshtastic.core.model.util.anonymize +import org.meshtastic.core.network.repository.NetworkRepository import org.meshtastic.core.repository.PlatformAnalytics import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.repository.RadioPrefs import org.meshtastic.core.repository.RadioTransport -import org.meshtastic.feature.connections.repository.NetworkRepository import org.meshtastic.proto.Heartbeat import org.meshtastic.proto.ToRadio @@ -73,6 +73,7 @@ class AndroidRadioInterfaceService( private val dispatchers: CoroutineDispatchers, private val bluetoothRepository: BluetoothRepository, private val networkRepository: NetworkRepository, + private val buildConfigProvider: BuildConfigProvider, @Named("ProcessLifecycle") private val processLifecycle: Lifecycle, private val radioPrefs: RadioPrefs, private val interfaceFactory: Lazy, @@ -187,7 +188,7 @@ class AndroidRadioInterfaceService( interfaceFactory.value.toInterfaceAddress(interfaceId, rest) override fun isMockInterface(): Boolean = - BuildConfig.DEBUG || Settings.System.getString(context.contentResolver, "firebase.test.lab") == "true" + buildConfigProvider.isDebug || Settings.System.getString(context.contentResolver, "firebase.test.lab") == "true" override fun getDeviceAddress(): String? { // If the user has unpaired our device, treat things as if we don't have one diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/BleRadioInterface.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/BleRadioInterface.kt similarity index 99% rename from app/src/main/kotlin/org/meshtastic/app/repository/radio/BleRadioInterface.kt rename to core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/BleRadioInterface.kt index b37fa1c53..af4b9f320 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/BleRadioInterface.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/BleRadioInterface.kt @@ -14,7 +14,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.repository.radio +@file:Suppress("TooManyFunctions", "TooGenericExceptionCaught") + +package org.meshtastic.core.network.radio import android.annotation.SuppressLint import co.touchlab.kermit.Logger diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/BleRadioInterfaceFactory.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/BleRadioInterfaceFactory.kt similarity index 97% rename from app/src/main/kotlin/org/meshtastic/app/repository/radio/BleRadioInterfaceFactory.kt rename to core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/BleRadioInterfaceFactory.kt index 341fe1afe..26956824c 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/BleRadioInterfaceFactory.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/BleRadioInterfaceFactory.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.repository.radio +package org.meshtastic.core.network.radio import org.koin.core.annotation.Single import org.meshtastic.core.ble.BleConnectionFactory diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/BleRadioInterfaceSpec.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/BleRadioInterfaceSpec.kt similarity index 97% rename from app/src/main/kotlin/org/meshtastic/app/repository/radio/BleRadioInterfaceSpec.kt rename to core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/BleRadioInterfaceSpec.kt index aaa39b9bd..461ac4b65 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/BleRadioInterfaceSpec.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/BleRadioInterfaceSpec.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.repository.radio +package org.meshtastic.core.network.radio import org.koin.core.annotation.Single import org.meshtastic.core.repository.RadioInterfaceService diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceFactory.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/InterfaceFactory.kt similarity index 98% rename from app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceFactory.kt rename to core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/InterfaceFactory.kt index 91f16e0d9..47a1365d2 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceFactory.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/InterfaceFactory.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.repository.radio +package org.meshtastic.core.network.radio import org.koin.core.annotation.Single import org.meshtastic.core.model.InterfaceId diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterface.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/SerialInterface.kt similarity index 95% rename from app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterface.kt rename to core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/SerialInterface.kt index c1f509499..2e97cff75 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterface.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/SerialInterface.kt @@ -14,14 +14,14 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.repository.radio +package org.meshtastic.core.network.radio import co.touchlab.kermit.Logger import org.meshtastic.core.common.util.nowMillis +import org.meshtastic.core.network.repository.SerialConnection +import org.meshtastic.core.network.repository.SerialConnectionListener +import org.meshtastic.core.network.repository.UsbRepository import org.meshtastic.core.repository.RadioInterfaceService -import org.meshtastic.feature.connections.repository.SerialConnection -import org.meshtastic.feature.connections.repository.SerialConnectionListener -import org.meshtastic.feature.connections.repository.UsbRepository import java.util.concurrent.atomic.AtomicReference /** An interface that assumes we are talking to a meshtastic device via USB serial */ diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterfaceFactory.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/SerialInterfaceFactory.kt similarity index 90% rename from app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterfaceFactory.kt rename to core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/SerialInterfaceFactory.kt index c7a123cc3..f8c53313b 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterfaceFactory.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/SerialInterfaceFactory.kt @@ -14,11 +14,11 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.repository.radio +package org.meshtastic.core.network.radio import org.koin.core.annotation.Single +import org.meshtastic.core.network.repository.UsbRepository import org.meshtastic.core.repository.RadioInterfaceService -import org.meshtastic.feature.connections.repository.UsbRepository /** Factory for creating `SerialInterface` instances. */ @Single diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterfaceSpec.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/SerialInterfaceSpec.kt similarity index 94% rename from app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterfaceSpec.kt rename to core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/SerialInterfaceSpec.kt index 54a44485b..8597fd060 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/SerialInterfaceSpec.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/SerialInterfaceSpec.kt @@ -14,13 +14,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.repository.radio +package org.meshtastic.core.network.radio import android.hardware.usb.UsbManager import com.hoho.android.usbserial.driver.UsbSerialDriver import org.koin.core.annotation.Single +import org.meshtastic.core.network.repository.UsbRepository import org.meshtastic.core.repository.RadioInterfaceService -import org.meshtastic.feature.connections.repository.UsbRepository /** Serial/USB interface backend implementation. */ @Single diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterface.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/TCPInterface.kt similarity index 98% rename from app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterface.kt rename to core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/TCPInterface.kt index 8217302ce..adab96d4d 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterface.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/TCPInterface.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.repository.radio +package org.meshtastic.core.network.radio import co.touchlab.kermit.Logger import org.meshtastic.core.common.util.handledLaunch diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterfaceFactory.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/TCPInterfaceFactory.kt similarity index 96% rename from app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterfaceFactory.kt rename to core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/TCPInterfaceFactory.kt index b11916940..003294448 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterfaceFactory.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/TCPInterfaceFactory.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.repository.radio +package org.meshtastic.core.network.radio import org.koin.core.annotation.Single import org.meshtastic.core.di.CoroutineDispatchers diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterfaceSpec.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/TCPInterfaceSpec.kt similarity index 96% rename from app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterfaceSpec.kt rename to core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/TCPInterfaceSpec.kt index b48ee826c..2539bc13c 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/TCPInterfaceSpec.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/radio/TCPInterfaceSpec.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.repository.radio +package org.meshtastic.core.network.radio import org.koin.core.annotation.Single import org.meshtastic.core.repository.RadioInterfaceService diff --git a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/ConnectivityManager.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/ConnectivityManager.kt similarity index 97% rename from feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/ConnectivityManager.kt rename to core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/ConnectivityManager.kt index e245f2419..559b873d3 100644 --- a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/ConnectivityManager.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/ConnectivityManager.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.feature.connections.repository +package org.meshtastic.core.network.repository import android.net.ConnectivityManager import android.net.Network diff --git a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/NetworkRepository.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/NetworkRepository.kt similarity index 98% rename from feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/NetworkRepository.kt rename to core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/NetworkRepository.kt index f44f7f173..2e0f797ef 100644 --- a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/NetworkRepository.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/NetworkRepository.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.feature.connections.repository +package org.meshtastic.core.network.repository import android.net.ConnectivityManager import android.net.nsd.NsdManager diff --git a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/NsdManager.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/NsdManager.kt similarity index 98% rename from feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/NsdManager.kt rename to core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/NsdManager.kt index 6e7bf2eec..ce272bf59 100644 --- a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/NsdManager.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/NsdManager.kt @@ -14,7 +14,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.feature.connections.repository +@file:Suppress("SwallowedException") + +package org.meshtastic.core.network.repository import android.annotation.SuppressLint import android.net.nsd.NsdManager diff --git a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/ProbeTableProvider.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/ProbeTableProvider.kt similarity index 94% rename from feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/ProbeTableProvider.kt rename to core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/ProbeTableProvider.kt index 7d091f2ff..15558118e 100644 --- a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/ProbeTableProvider.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/ProbeTableProvider.kt @@ -14,7 +14,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.feature.connections.repository +@file:Suppress("MagicNumber") + +package org.meshtastic.core.network.repository import com.hoho.android.usbserial.driver.CdcAcmSerialDriver import com.hoho.android.usbserial.driver.ProbeTable diff --git a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/SerialConnection.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/SerialConnection.kt similarity index 95% rename from feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/SerialConnection.kt rename to core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/SerialConnection.kt index cb9dc679b..2ec10b7f1 100644 --- a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/SerialConnection.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/SerialConnection.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.feature.connections.repository +package org.meshtastic.core.network.repository /** USB serial connection. */ interface SerialConnection : AutoCloseable { diff --git a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/SerialConnectionImpl.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/SerialConnectionImpl.kt similarity index 98% rename from feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/SerialConnectionImpl.kt rename to core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/SerialConnectionImpl.kt index a06d5492d..b2ccf6545 100644 --- a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/SerialConnectionImpl.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/SerialConnectionImpl.kt @@ -14,7 +14,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.feature.connections.repository +@file:Suppress("MagicNumber") + +package org.meshtastic.core.network.repository import android.hardware.usb.UsbManager import co.touchlab.kermit.Logger diff --git a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/SerialConnectionListener.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/SerialConnectionListener.kt similarity index 95% rename from feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/SerialConnectionListener.kt rename to core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/SerialConnectionListener.kt index 4dbc2b90d..b56236f5b 100644 --- a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/SerialConnectionListener.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/SerialConnectionListener.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.feature.connections.repository +package org.meshtastic.core.network.repository /** Callbacks indicating state changes in the USB serial connection. */ interface SerialConnectionListener { diff --git a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/UsbBroadcastReceiver.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/UsbBroadcastReceiver.kt similarity index 97% rename from feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/UsbBroadcastReceiver.kt rename to core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/UsbBroadcastReceiver.kt index d472e3bf8..79d09639a 100644 --- a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/UsbBroadcastReceiver.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/UsbBroadcastReceiver.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.feature.connections.repository +package org.meshtastic.core.network.repository import android.content.BroadcastReceiver import android.content.Context diff --git a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/UsbManager.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/UsbManager.kt similarity index 97% rename from feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/UsbManager.kt rename to core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/UsbManager.kt index 66b3bb515..b36c5c3e9 100644 --- a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/UsbManager.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/UsbManager.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.feature.connections.repository +package org.meshtastic.core.network.repository import android.content.BroadcastReceiver import android.content.Context diff --git a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/UsbRepository.kt b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/UsbRepository.kt similarity index 98% rename from feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/UsbRepository.kt rename to core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/UsbRepository.kt index e73871336..b4773dff3 100644 --- a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/repository/UsbRepository.kt +++ b/core/network/src/androidMain/kotlin/org/meshtastic/core/network/repository/UsbRepository.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.feature.connections.repository +package org.meshtastic.core.network.repository import android.app.Application import android.hardware.usb.UsbDevice diff --git a/core/network/src/androidUnitTest/kotlin/org/meshtastic/core/network/radio/BleRadioInterfaceTest.kt b/core/network/src/androidUnitTest/kotlin/org/meshtastic/core/network/radio/BleRadioInterfaceTest.kt index 706a47340..457a3a9d9 100644 --- a/core/network/src/androidUnitTest/kotlin/org/meshtastic/core/network/radio/BleRadioInterfaceTest.kt +++ b/core/network/src/androidUnitTest/kotlin/org/meshtastic/core/network/radio/BleRadioInterfaceTest.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.repository.radio +package org.meshtastic.core.network.radio import io.mockk.coEvery import io.mockk.every diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceFactorySpi.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/InterfaceFactorySpi.kt similarity index 96% rename from app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceFactorySpi.kt rename to core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/InterfaceFactorySpi.kt index b9856af82..5354f5500 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceFactorySpi.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/InterfaceFactorySpi.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.repository.radio +package org.meshtastic.core.network.radio import org.meshtastic.core.repository.RadioTransport diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceSpec.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/InterfaceSpec.kt similarity index 96% rename from app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceSpec.kt rename to core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/InterfaceSpec.kt index 7ac3619da..aec9ec667 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/InterfaceSpec.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/InterfaceSpec.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.repository.radio +package org.meshtastic.core.network.radio import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.repository.RadioTransport diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterface.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockInterface.kt similarity index 99% rename from app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterface.kt rename to core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockInterface.kt index 776729bba..8de3000af 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterface.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockInterface.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.repository.radio +package org.meshtastic.core.network.radio import co.touchlab.kermit.Logger import kotlinx.coroutines.delay diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterfaceFactory.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockInterfaceFactory.kt similarity index 95% rename from app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterfaceFactory.kt rename to core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockInterfaceFactory.kt index 5f8328d3a..492b5782c 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterfaceFactory.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockInterfaceFactory.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.repository.radio +package org.meshtastic.core.network.radio import org.koin.core.annotation.Single import org.meshtastic.core.repository.RadioInterfaceService diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterfaceSpec.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockInterfaceSpec.kt similarity index 96% rename from app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterfaceSpec.kt rename to core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockInterfaceSpec.kt index 13dcadd50..0f77cb5dc 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/MockInterfaceSpec.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockInterfaceSpec.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.repository.radio +package org.meshtastic.core.network.radio import org.koin.core.annotation.Single import org.meshtastic.core.repository.RadioInterfaceService diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterface.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/NopInterface.kt similarity index 95% rename from app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterface.kt rename to core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/NopInterface.kt index e9eed976a..27348635c 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterface.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/NopInterface.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.repository.radio +package org.meshtastic.core.network.radio import org.meshtastic.core.repository.RadioTransport diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterfaceFactory.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/NopInterfaceFactory.kt similarity index 95% rename from app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterfaceFactory.kt rename to core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/NopInterfaceFactory.kt index 56d58b846..5d9991e34 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterfaceFactory.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/NopInterfaceFactory.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.repository.radio +package org.meshtastic.core.network.radio import org.koin.core.annotation.Single diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterfaceSpec.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/NopInterfaceSpec.kt similarity index 96% rename from app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterfaceSpec.kt rename to core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/NopInterfaceSpec.kt index 149a2469a..df77578bf 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/NopInterfaceSpec.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/NopInterfaceSpec.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.repository.radio +package org.meshtastic.core.network.radio import org.koin.core.annotation.Single import org.meshtastic.core.repository.RadioInterfaceService diff --git a/app/src/main/kotlin/org/meshtastic/app/repository/radio/StreamInterface.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/StreamInterface.kt similarity index 98% rename from app/src/main/kotlin/org/meshtastic/app/repository/radio/StreamInterface.kt rename to core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/StreamInterface.kt index 477bd50d2..7414def38 100644 --- a/app/src/main/kotlin/org/meshtastic/app/repository/radio/StreamInterface.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/StreamInterface.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.repository.radio +package org.meshtastic.core.network.radio import co.touchlab.kermit.Logger import kotlinx.coroutines.launch diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/repository/NetworkConstants.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/NetworkConstants.kt similarity index 93% rename from feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/repository/NetworkConstants.kt rename to core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/NetworkConstants.kt index 8a7cab5b6..e35abf554 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/repository/NetworkConstants.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/NetworkConstants.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.feature.connections.repository +package org.meshtastic.core.network.repository object NetworkConstants { const val SERVICE_PORT = 4403 diff --git a/app/src/main/res/raw/alert.mp3 b/core/resources/src/androidMain/res/raw/alert.mp3 similarity index 100% rename from app/src/main/res/raw/alert.mp3 rename to core/resources/src/androidMain/res/raw/alert.mp3 diff --git a/core/service/build.gradle.kts b/core/service/build.gradle.kts index 89476bb13..0d0b11699 100644 --- a/core/service/build.gradle.kts +++ b/core/service/build.gradle.kts @@ -36,6 +36,7 @@ kotlin { implementation(projects.core.data) implementation(projects.core.database) implementation(projects.core.model) + implementation(projects.core.navigation) implementation(projects.core.prefs) implementation(projects.core.proto) diff --git a/app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshWorkerManager.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidMeshWorkerManager.kt similarity index 93% rename from app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshWorkerManager.kt rename to core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidMeshWorkerManager.kt index 25e88a9ff..32530dcf4 100644 --- a/app/src/main/kotlin/org/meshtastic/app/service/AndroidMeshWorkerManager.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidMeshWorkerManager.kt @@ -14,15 +14,15 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.service +package org.meshtastic.core.service import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import androidx.work.workDataOf import org.koin.core.annotation.Single -import org.meshtastic.app.messaging.domain.worker.SendMessageWorker import org.meshtastic.core.repository.MeshWorkerManager +import org.meshtastic.core.service.worker.SendMessageWorker @Single class AndroidMeshWorkerManager(private val workManager: WorkManager) : MeshWorkerManager { diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt index b6a1b7273..cd4b317bd 100644 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/AndroidRadioControllerImpl.kt @@ -200,7 +200,7 @@ class AndroidRadioControllerImpl( // Ensure service is running/restarted to handle the new address val intent = android.content.Intent().apply { - setClassName("com.geeksville.mesh", "org.meshtastic.app.service.MeshService") + setClassName("com.geeksville.mesh", "org.meshtastic.core.service.MeshService") } context.startForegroundService(intent) } diff --git a/app/src/main/kotlin/org/meshtastic/app/service/BootCompleteReceiver.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/BootCompleteReceiver.kt similarity index 97% rename from app/src/main/kotlin/org/meshtastic/app/service/BootCompleteReceiver.kt rename to core/service/src/androidMain/kotlin/org/meshtastic/core/service/BootCompleteReceiver.kt index 732be7b19..b01475b6d 100644 --- a/app/src/main/kotlin/org/meshtastic/app/service/BootCompleteReceiver.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/BootCompleteReceiver.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.service +package org.meshtastic.core.service import android.content.BroadcastReceiver import android.content.Context diff --git a/app/src/main/kotlin/org/meshtastic/app/service/Constants.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/Constants.kt similarity index 98% rename from app/src/main/kotlin/org/meshtastic/app/service/Constants.kt rename to core/service/src/androidMain/kotlin/org/meshtastic/core/service/Constants.kt index af5fdbdcd..4e0b5e7b8 100644 --- a/app/src/main/kotlin/org/meshtastic/app/service/Constants.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/Constants.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.service +package org.meshtastic.core.service import org.meshtastic.core.api.MeshtasticIntent diff --git a/app/src/main/kotlin/org/meshtastic/app/service/MarkAsReadReceiver.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MarkAsReadReceiver.kt similarity index 98% rename from app/src/main/kotlin/org/meshtastic/app/service/MarkAsReadReceiver.kt rename to core/service/src/androidMain/kotlin/org/meshtastic/core/service/MarkAsReadReceiver.kt index ebe68c74d..966569f4f 100644 --- a/app/src/main/kotlin/org/meshtastic/app/service/MarkAsReadReceiver.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MarkAsReadReceiver.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.service +package org.meshtastic.core.service import android.content.BroadcastReceiver import android.content.Context diff --git a/app/src/main/kotlin/org/meshtastic/app/service/MeshService.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshService.kt similarity index 90% rename from app/src/main/kotlin/org/meshtastic/app/service/MeshService.kt rename to core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshService.kt index afd31361c..2ed00ec6a 100644 --- a/app/src/main/kotlin/org/meshtastic/app/service/MeshService.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshService.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.service +package org.meshtastic.core.service import android.app.Service import android.content.Context @@ -27,12 +27,8 @@ import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import org.koin.android.ext.android.inject -import org.meshtastic.app.BuildConfig import org.meshtastic.core.common.hasLocationPermission -import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.common.util.toRemoteExceptions import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.DeviceVersion @@ -44,17 +40,12 @@ import org.meshtastic.core.model.RadioNotConnectedException import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.MeshConnectionManager import org.meshtastic.core.repository.MeshLocationManager -import org.meshtastic.core.repository.MeshMessageProcessor import org.meshtastic.core.repository.MeshRouter -import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.repository.NodeManager -import org.meshtastic.core.repository.PacketHandler import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.repository.SERVICE_NOTIFY_ID import org.meshtastic.core.repository.ServiceBroadcasts import org.meshtastic.core.repository.ServiceRepository -import org.meshtastic.core.service.IMeshService -import org.meshtastic.feature.connections.NO_DEVICE_SELECTED import org.meshtastic.proto.PortNum @Suppress("TooManyFunctions", "LargeClass") @@ -64,21 +55,17 @@ class MeshService : Service() { private val serviceRepository: ServiceRepository by inject() - private val packetHandler: PacketHandler by inject() - private val serviceBroadcasts: ServiceBroadcasts by inject() private val nodeManager: NodeManager by inject() - private val messageProcessor: MeshMessageProcessor by inject() - private val commandSender: CommandSender by inject() private val locationManager: MeshLocationManager by inject() private val connectionManager: MeshConnectionManager by inject() - private val serviceNotifications: MeshServiceNotifications by inject() + private val orchestrator: MeshServiceOrchestrator by inject() private val router: MeshRouter by inject() @@ -102,8 +89,8 @@ class MeshService : Service() { startService(context) } - val minDeviceVersion = DeviceVersion(BuildConfig.MIN_FW_VERSION) - val absoluteMinDeviceVersion = DeviceVersion(BuildConfig.ABS_MIN_FW_VERSION) + val minDeviceVersion = DeviceVersion(DeviceVersion.MIN_FW_VERSION) + val absoluteMinDeviceVersion = DeviceVersion(DeviceVersion.ABS_MIN_FW_VERSION) } override fun onCreate() { @@ -121,29 +108,13 @@ class MeshService : Service() { throw e } Logger.i { "Creating mesh service" } - serviceNotifications.initChannels() - packetHandler.start(serviceScope) - router.start(serviceScope) - nodeManager.start(serviceScope) - connectionManager.start(serviceScope) - messageProcessor.start(serviceScope) - commandSender.start(serviceScope) - - serviceScope.handledLaunch { radioInterfaceService.connect() } - - radioInterfaceService.receivedData - .onEach { bytes -> messageProcessor.handleFromRadio(bytes, nodeManager.myNodeNum) } - .launchIn(serviceScope) - - serviceRepository.serviceAction.onEach(router.actionHandler::onServiceAction).launchIn(serviceScope) - - nodeManager.loadCachedNodeDB() + orchestrator.start() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val a = radioInterfaceService.getDeviceAddress() - val wantForeground = a != null && a != NO_DEVICE_SELECTED + val wantForeground = a != null && a != "n" val notification = connectionManager.updateStatusNotification() as android.app.Notification @@ -207,6 +178,7 @@ class MeshService : Service() { override fun onDestroy() { Logger.i { "Destroying mesh service" } ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) + orchestrator.stop() serviceJob.cancel() super.onDestroy() } diff --git a/app/src/main/kotlin/org/meshtastic/app/service/MeshServiceNotificationsImpl.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshServiceNotificationsImpl.kt similarity index 97% rename from app/src/main/kotlin/org/meshtastic/app/service/MeshServiceNotificationsImpl.kt rename to core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshServiceNotificationsImpl.kt index e790d8d0d..ea17e4fc0 100644 --- a/app/src/main/kotlin/org/meshtastic/app/service/MeshServiceNotificationsImpl.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshServiceNotificationsImpl.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.service +package org.meshtastic.core.service import android.app.Notification import android.app.NotificationChannel @@ -40,11 +40,6 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import org.jetbrains.compose.resources.StringResource import org.koin.core.annotation.Single -import org.meshtastic.app.MainActivity -import org.meshtastic.app.R.raw -import org.meshtastic.app.service.MarkAsReadReceiver.Companion.MARK_AS_READ_ACTION -import org.meshtastic.app.service.ReactionReceiver.Companion.REACT_ACTION -import org.meshtastic.app.service.ReplyReceiver.Companion.KEY_TEXT_REPLY import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.Message @@ -55,6 +50,7 @@ import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.SERVICE_NOTIFY_ID +import org.meshtastic.core.resources.R.raw import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.client_notification import org.meshtastic.core.resources.getString @@ -87,6 +83,9 @@ import org.meshtastic.core.resources.no_local_stats import org.meshtastic.core.resources.powered import org.meshtastic.core.resources.reply import org.meshtastic.core.resources.you +import org.meshtastic.core.service.MarkAsReadReceiver.Companion.MARK_AS_READ_ACTION +import org.meshtastic.core.service.ReactionReceiver.Companion.REACT_ACTION +import org.meshtastic.core.service.ReplyReceiver.Companion.KEY_TEXT_REPLY import org.meshtastic.proto.ClientNotification import org.meshtastic.proto.DeviceMetrics import org.meshtastic.proto.LocalStats @@ -453,7 +452,7 @@ class MeshServiceNotificationsImpl( val summaryNotification = commonBuilder(NotificationType.DirectMessage) - .setSmallIcon(org.meshtastic.app.R.drawable.app_icon) + .setSmallIcon(context.applicationInfo.icon) .setStyle(messagingStyle) .setGroup(GROUP_KEY_MESSAGES) .setGroupSummary(true) @@ -697,14 +696,17 @@ class MeshServiceNotificationsImpl( // region Helper/Builder Methods private val openAppIntent: PendingIntent by lazy { - val intent = Intent(context, MainActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_SINGLE_TOP } + val intent = + Intent(context, Class.forName("org.meshtastic.app.MainActivity")).apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP + } PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) } private fun createOpenMessageIntent(contactKey: String): PendingIntent { val deepLinkUri = "$DEEP_LINK_BASE_URI/messages/$contactKey".toUri() val deepLinkIntent = - Intent(Intent.ACTION_VIEW, deepLinkUri, context, MainActivity::class.java).apply { + Intent(Intent.ACTION_VIEW, deepLinkUri, context, Class.forName("org.meshtastic.app.MainActivity")).apply { flags = Intent.FLAG_ACTIVITY_SINGLE_TOP } @@ -717,7 +719,7 @@ class MeshServiceNotificationsImpl( private fun createOpenWaypointIntent(waypointId: Int): PendingIntent { val deepLinkUri = "$DEEP_LINK_BASE_URI/map?waypointId=$waypointId".toUri() val deepLinkIntent = - Intent(Intent.ACTION_VIEW, deepLinkUri, context, MainActivity::class.java).apply { + Intent(Intent.ACTION_VIEW, deepLinkUri, context, Class.forName("org.meshtastic.app.MainActivity")).apply { flags = Intent.FLAG_ACTIVITY_SINGLE_TOP } @@ -730,7 +732,7 @@ class MeshServiceNotificationsImpl( private fun createOpenNodeDetailIntent(nodeNum: Int): PendingIntent { val deepLinkUri = "$DEEP_LINK_BASE_URI/node?destNum=$nodeNum".toUri() val deepLinkIntent = - Intent(Intent.ACTION_VIEW, deepLinkUri, context, MainActivity::class.java).apply { + Intent(Intent.ACTION_VIEW, deepLinkUri, context, Class.forName("org.meshtastic.app.MainActivity")).apply { flags = Intent.FLAG_ACTIVITY_SINGLE_TOP } @@ -811,7 +813,7 @@ class MeshServiceNotificationsImpl( type: NotificationType, contentIntent: PendingIntent? = null, ): NotificationCompat.Builder { - val smallIcon = org.meshtastic.app.R.drawable.app_icon + val smallIcon = context.applicationInfo.icon return NotificationCompat.Builder(context, type.channelId) .setSmallIcon(smallIcon) diff --git a/app/src/main/kotlin/org/meshtastic/app/service/MeshServiceStarter.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshServiceStarter.kt similarity index 92% rename from app/src/main/kotlin/org/meshtastic/app/service/MeshServiceStarter.kt rename to core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshServiceStarter.kt index 96ea0d9bf..463ec35ea 100644 --- a/app/src/main/kotlin/org/meshtastic/app/service/MeshServiceStarter.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshServiceStarter.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.service +package org.meshtastic.core.service import android.app.ForegroundServiceStartNotAllowedException import android.content.Context @@ -23,8 +23,7 @@ import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OutOfQuotaPolicy import androidx.work.WorkManager import co.touchlab.kermit.Logger -import org.meshtastic.app.BuildConfig -import org.meshtastic.app.worker.ServiceKeepAliveWorker +import org.meshtastic.core.service.worker.ServiceKeepAliveWorker // / Helper function to start running our service fun MeshService.Companion.startService(context: Context) { @@ -36,7 +35,7 @@ fun MeshService.Companion.startService(context: Context) { // Before binding we want to explicitly create - so the service stays alive forever (so it can keep // listening for the bluetooth packets arriving from the radio. And when they arrive forward them // to Signal or whatever. - Logger.i { "Trying to start service debug=${BuildConfig.DEBUG}" } + Logger.i { "Trying to start service debug=${false}" } val intent = createIntent(context) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { diff --git a/app/src/main/kotlin/org/meshtastic/app/service/ReactionReceiver.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ReactionReceiver.kt similarity index 98% rename from app/src/main/kotlin/org/meshtastic/app/service/ReactionReceiver.kt rename to core/service/src/androidMain/kotlin/org/meshtastic/core/service/ReactionReceiver.kt index fec13effb..7a3e026a7 100644 --- a/app/src/main/kotlin/org/meshtastic/app/service/ReactionReceiver.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ReactionReceiver.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.service +package org.meshtastic.core.service import android.content.BroadcastReceiver import android.content.Context diff --git a/app/src/main/kotlin/org/meshtastic/app/service/ReplyReceiver.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ReplyReceiver.kt similarity index 98% rename from app/src/main/kotlin/org/meshtastic/app/service/ReplyReceiver.kt rename to core/service/src/androidMain/kotlin/org/meshtastic/core/service/ReplyReceiver.kt index e09f6c656..4e82a735d 100644 --- a/app/src/main/kotlin/org/meshtastic/app/service/ReplyReceiver.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ReplyReceiver.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.service +package org.meshtastic.core.service import android.content.BroadcastReceiver import android.content.Context diff --git a/app/src/main/kotlin/org/meshtastic/app/service/ServiceBroadcasts.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ServiceBroadcasts.kt similarity index 99% rename from app/src/main/kotlin/org/meshtastic/app/service/ServiceBroadcasts.kt rename to core/service/src/androidMain/kotlin/org/meshtastic/core/service/ServiceBroadcasts.kt index 8b4ffc1a2..321968908 100644 --- a/app/src/main/kotlin/org/meshtastic/app/service/ServiceBroadcasts.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/ServiceBroadcasts.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.service +package org.meshtastic.core.service import android.content.Context import android.content.Intent diff --git a/app/src/main/kotlin/org/meshtastic/app/worker/MeshLogCleanupWorker.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/worker/MeshLogCleanupWorker.kt similarity index 98% rename from app/src/main/kotlin/org/meshtastic/app/worker/MeshLogCleanupWorker.kt rename to core/service/src/androidMain/kotlin/org/meshtastic/core/service/worker/MeshLogCleanupWorker.kt index 11495b645..ed686d984 100644 --- a/app/src/main/kotlin/org/meshtastic/app/worker/MeshLogCleanupWorker.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/worker/MeshLogCleanupWorker.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.worker +package org.meshtastic.core.service.worker import android.content.Context import androidx.work.CoroutineWorker diff --git a/app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/SendMessageWorker.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/worker/SendMessageWorker.kt similarity index 97% rename from app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/SendMessageWorker.kt rename to core/service/src/androidMain/kotlin/org/meshtastic/core/service/worker/SendMessageWorker.kt index 19fb3324e..c12957eb7 100644 --- a/app/src/main/kotlin/org/meshtastic/app/messaging/domain/worker/SendMessageWorker.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/worker/SendMessageWorker.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.messaging.domain.worker +package org.meshtastic.core.service.worker import android.content.Context import androidx.work.CoroutineWorker diff --git a/app/src/main/kotlin/org/meshtastic/app/worker/ServiceKeepAliveWorker.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/worker/ServiceKeepAliveWorker.kt similarity index 93% rename from app/src/main/kotlin/org/meshtastic/app/worker/ServiceKeepAliveWorker.kt rename to core/service/src/androidMain/kotlin/org/meshtastic/core/service/worker/ServiceKeepAliveWorker.kt index b83fc9aff..9bda51e00 100644 --- a/app/src/main/kotlin/org/meshtastic/app/worker/ServiceKeepAliveWorker.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/worker/ServiceKeepAliveWorker.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.app.worker +package org.meshtastic.core.service.worker import android.app.Notification import android.content.Context @@ -26,11 +26,10 @@ import androidx.work.ForegroundInfo import androidx.work.WorkerParameters import co.touchlab.kermit.Logger import org.koin.android.annotation.KoinWorker -import org.meshtastic.app.R -import org.meshtastic.app.service.MeshService -import org.meshtastic.app.service.startService import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.repository.SERVICE_NOTIFY_ID +import org.meshtastic.core.service.MeshService +import org.meshtastic.core.service.startService /** * A worker whose sole purpose is to start the MeshService from the background. This is used as a fallback when @@ -81,7 +80,7 @@ class ServiceKeepAliveWorker( // We use "my_service" which matches NotificationType.ServiceState.channelId in MeshServiceNotificationsImpl return NotificationCompat.Builder(applicationContext, "my_service") - .setSmallIcon(R.drawable.ic_launcher_foreground) + .setSmallIcon(applicationContext.applicationInfo.icon) .setContentTitle("Resuming Mesh Service") .setPriority(NotificationCompat.PRIORITY_LOW) .setOngoing(true) diff --git a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/MeshServiceOrchestrator.kt b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/MeshServiceOrchestrator.kt index 0bcfb62d6..0faf332a8 100644 --- a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/MeshServiceOrchestrator.kt +++ b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/MeshServiceOrchestrator.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import org.koin.core.annotation.Single import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.MeshConnectionManager @@ -42,6 +43,7 @@ import org.meshtastic.core.repository.ServiceRepository * All injected dependencies are `commonMain` interfaces with real implementations in `core:data`. */ @Suppress("LongParameterList") +@Single class MeshServiceOrchestrator( private val radioInterfaceService: RadioInterfaceService, private val serviceRepository: ServiceRepository, diff --git a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/MeshServiceOrchestratorTest.kt b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/MeshServiceOrchestratorTest.kt new file mode 100644 index 000000000..3afc27cd5 --- /dev/null +++ b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/MeshServiceOrchestratorTest.kt @@ -0,0 +1,77 @@ +/* + * 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.service + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.flow.MutableSharedFlow +import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.MeshConnectionManager +import org.meshtastic.core.repository.MeshMessageProcessor +import org.meshtastic.core.repository.MeshRouter +import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.NodeManager +import org.meshtastic.core.repository.PacketHandler +import org.meshtastic.core.repository.RadioInterfaceService +import org.meshtastic.core.repository.ServiceRepository +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class MeshServiceOrchestratorTest { + + @Test + fun testStartWiresComponents() { + val radioInterfaceService = mockk(relaxed = true) + val serviceRepository = mockk(relaxed = true) + val packetHandler = mockk(relaxed = true) + val nodeManager = mockk(relaxed = true) + val messageProcessor = mockk(relaxed = true) + val commandSender = mockk(relaxed = true) + val connectionManager = mockk(relaxed = true) + val router = mockk(relaxed = true) + val serviceNotifications = mockk(relaxed = true) + + every { radioInterfaceService.receivedData } returns MutableSharedFlow() + every { serviceRepository.serviceAction } returns MutableSharedFlow() + + val orchestrator = + MeshServiceOrchestrator( + radioInterfaceService, + serviceRepository, + packetHandler, + nodeManager, + messageProcessor, + commandSender, + connectionManager, + router, + serviceNotifications, + ) + + assertFalse(orchestrator.isRunning) + orchestrator.start() + assertTrue(orchestrator.isRunning) + + verify { serviceNotifications.initChannels() } + verify { packetHandler.start(any()) } + verify { nodeManager.loadCachedNodeDB() } + + orchestrator.stop() + assertFalse(orchestrator.isRunning) + } +} diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/Main.kt b/desktop/src/main/kotlin/org/meshtastic/desktop/Main.kt index c1555c5db..4a8bd17ef 100644 --- a/desktop/src/main/kotlin/org/meshtastic/desktop/Main.kt +++ b/desktop/src/main/kotlin/org/meshtastic/desktop/Main.kt @@ -49,11 +49,11 @@ import org.koin.core.context.startKoin import org.meshtastic.core.datastore.UiPreferencesDataSource import org.meshtastic.core.navigation.SettingsRoutes import org.meshtastic.core.navigation.TopLevelDestination +import org.meshtastic.core.service.MeshServiceOrchestrator import org.meshtastic.core.ui.theme.AppTheme import org.meshtastic.desktop.data.DesktopPreferencesDataSource import org.meshtastic.desktop.di.desktopModule import org.meshtastic.desktop.di.desktopPlatformModule -import org.meshtastic.desktop.radio.DesktopMeshServiceController import org.meshtastic.desktop.ui.DesktopMainScreen import org.meshtastic.desktop.ui.navSavedStateConfig import java.util.Locale @@ -82,7 +82,7 @@ fun main() = application(exitProcessOnExit = false) { val systemLocale = remember { Locale.getDefault() } // Start the mesh service processing chain (desktop equivalent of Android's MeshService) - val meshServiceController = remember { koinApp.koin.get() } + val meshServiceController = remember { koinApp.koin.get() } DisposableEffect(Unit) { meshServiceController.start() onDispose { meshServiceController.stop() } diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt b/desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt index edaea3c50..2bc65cb0b 100644 --- a/desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt +++ b/desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt @@ -41,7 +41,6 @@ import org.meshtastic.core.repository.PlatformAnalytics import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.repository.ServiceBroadcasts import org.meshtastic.core.repository.ServiceRepository -import org.meshtastic.desktop.radio.DesktopMeshServiceController import org.meshtastic.desktop.radio.DesktopRadioInterfaceService import org.meshtastic.desktop.stub.NoopAppWidgetUpdater import org.meshtastic.desktop.stub.NoopCompassHeadingProvider @@ -151,19 +150,6 @@ private fun desktopPlatformStubsModule() = module { single { NoopMagneticFieldProvider() } // Desktop mesh service controller — replaces Android's MeshService lifecycle - single { - DesktopMeshServiceController( - radioInterfaceService = get(), - serviceRepository = get(), - messageProcessor = get(), - connectionManager = get(), - packetHandler = get(), - router = get(), - nodeManager = get(), - commandSender = get(), - ) - } - // Ktor HttpClient for JVM/Desktop (equivalent of CoreNetworkAndroidModule on Android) single { HttpClient(Java) { install(ContentNegotiation) { json(get()) } } } diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/radio/DesktopMeshServiceController.kt b/desktop/src/main/kotlin/org/meshtastic/desktop/radio/DesktopMeshServiceController.kt deleted file mode 100644 index f6f725778..000000000 --- a/desktop/src/main/kotlin/org/meshtastic/desktop/radio/DesktopMeshServiceController.kt +++ /dev/null @@ -1,110 +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.desktop.radio - -import co.touchlab.kermit.Logger -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import org.meshtastic.core.common.util.handledLaunch -import org.meshtastic.core.repository.CommandSender -import org.meshtastic.core.repository.MeshConnectionManager -import org.meshtastic.core.repository.MeshMessageProcessor -import org.meshtastic.core.repository.MeshRouter -import org.meshtastic.core.repository.NodeManager -import org.meshtastic.core.repository.PacketHandler -import org.meshtastic.core.repository.RadioInterfaceService -import org.meshtastic.core.repository.ServiceRepository - -/** - * Desktop equivalent of Android's `MeshService.onCreate()`. - * - * Starts the full message-processing chain that connects the radio transport layer to the business logic: - * ``` - * radioInterfaceService.receivedData - * → messageProcessor.handleFromRadio(bytes, myNodeNum) - * → FromRadioPacketHandler → MeshRouter/PacketHandler/etc. - * ``` - * - * On Android this chain runs inside an Android `Service` (foreground service with notifications). On Desktop there is - * no Android Service concept, so this controller manages the same lifecycle in-process, started at app launch time. - */ -@Suppress("LongParameterList") -class DesktopMeshServiceController( - private val radioInterfaceService: RadioInterfaceService, - private val serviceRepository: ServiceRepository, - private val messageProcessor: MeshMessageProcessor, - private val connectionManager: MeshConnectionManager, - private val packetHandler: PacketHandler, - private val router: MeshRouter, - private val nodeManager: NodeManager, - private val commandSender: CommandSender, -) { - private var serviceScope: CoroutineScope? = null - - /** - * Starts the mesh service processing chain. - * - * This should be called once at application startup (after Koin is initialized). It mirrors the initialization - * logic from `MeshService.onCreate()`. - */ - @Suppress("InjectDispatcher") - fun start() { - if (serviceScope != null) { - Logger.w { "DesktopMeshServiceController: Already started, ignoring duplicate start()" } - return - } - - Logger.i { "DesktopMeshServiceController: Starting mesh service processing chain" } - val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - serviceScope = scope - - // Start all processing components (same order as MeshService.onCreate) - packetHandler.start(scope) - router.start(scope) - nodeManager.start(scope) - connectionManager.start(scope) - messageProcessor.start(scope) - commandSender.start(scope) - - // Auto-connect to saved device address (mirrors MeshService.onCreate) - scope.handledLaunch { radioInterfaceService.connect() } - - // Wire the data flow: radio → message processor - radioInterfaceService.receivedData - .onEach { bytes -> messageProcessor.handleFromRadio(bytes, nodeManager.myNodeNum) } - .launchIn(scope) - - // Wire service actions to the router - serviceRepository.serviceAction.onEach(router.actionHandler::onServiceAction).launchIn(scope) - - // Load any cached node database - nodeManager.loadCachedNodeDB() - - Logger.i { "DesktopMeshServiceController: Processing chain started" } - } - - /** Stops the mesh service processing chain and cancels all coroutines. */ - fun stop() { - Logger.i { "DesktopMeshServiceController: Stopping" } - serviceScope?.cancel("DesktopMeshServiceController stopped") - serviceScope = null - } -} diff --git a/docs/kmp-status.md b/docs/kmp-status.md index 0659dedb9..2f5f2861f 100644 --- a/docs/kmp-status.md +++ b/docs/kmp-status.md @@ -120,7 +120,7 @@ Based on the latest codebase investigation, the following steps are proposed to - Parity tests exist in `core:navigation/commonTest` (`NavigationParityTest`) and `desktop/test` (`DesktopTopLevelDestinationParityTest`). - Remaining parity work is documented in [`decisions/navigation3-parity-2026-03.md`](./decisions/navigation3-parity-2026-03.md): serializer registration validation and platform exception tracking. -## Remaining App-Only ViewModels +## App Module Thinning Status All major ViewModels have now been extracted to `commonMain` and no longer rely on Android-specific subclasses. Platform-specific dependencies (like `android.net.Uri` or Location permissions) have been successfully isolated behind injected `core:repository` interfaces (e.g., `FileService`, `LocationService`). @@ -133,6 +133,18 @@ Extracted to shared `commonMain` (no longer app-only): - `ChannelViewModel` → `feature:settings/commonMain` - `NodeMapViewModel` → `feature:map/commonMain` +Extracted to core KMP modules (Android-specific implementations): +- Android Services, WorkManager Workers, and BroadcastReceivers → `core:service/androidMain` +- BLE, USB/Serial, TCP radio connections, and NsdManager → `core:network/androidMain` + +Remaining to be extracted from `:app` to achieve a true thin-shell module: +- Navigation routes (`ChannelsNavigation.kt`, `SettingsNavigation.kt`, etc.) +- Android App Widgets (`LocalStatsWidget.kt`, `AndroidAppWidgetUpdater.kt`) +- Message Queue implementation (`WorkManagerMessageQueue.kt`) +- Location provider bindings (`AndroidMeshLocationManager.kt`) +- Top-level UI composition (`ui/Main.kt`, `ui/node/AdaptiveNodeListScreen.kt`) +- Root Activity and Koin bootstrapping (`MainActivity.kt`, `MeshUtilApplication.kt`, `MeshServiceClient.kt`) + ## Prerelease Dependencies | Dependency | Version | Why | diff --git a/docs/roadmap.md b/docs/roadmap.md index 4174c7562..630984bc6 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -87,7 +87,8 @@ These items address structural gaps identified in the March 2026 architecture re 1. **App module thinning** — Extracted ChannelViewModel, NodeMapViewModel, NodeContextMenu, EmptyDetailPlaceholder to shared modules. - ✅ **Done:** Extracted remaining 5 ViewModels: `SettingsViewModel`, `RadioConfigViewModel`, `DebugViewModel`, `MetricsViewModel`, `UIViewModel` to shared KMP modules. - - **Next:** Extract service/worker/radio files from `app` to `core:service/androidMain` and `core:network/androidMain`. + - ✅ **Done:** Extracted service, worker, and radio files from `app` to `core:service/androidMain` and `core:network/androidMain`. + - **Next:** Extract remaining Android-specific files (e.g., Navigation files, App Widgets, message queues, and root Activity logic) out of `:app` to establish a truly thin app module. 2. **Serial/USB transport** — direct radio connection on Desktop via jSerialComm 3. **MQTT transport** — cloud relay operation (KMP, benefits all targets) 4. **Evaluate KMP-native mocking** — Evaluate `mockative` or similar to replace `mockk` in `commonMain` of `core:testing` for iOS readiness. diff --git a/feature/connections/build.gradle.kts b/feature/connections/build.gradle.kts index 6b43d6376..292ebfa15 100644 --- a/feature/connections/build.gradle.kts +++ b/feature/connections/build.gradle.kts @@ -50,6 +50,7 @@ kotlin { implementation(projects.core.service) implementation(projects.core.ui) implementation(projects.core.ble) + implementation(projects.core.network) implementation(projects.feature.settings) implementation(libs.jetbrains.lifecycle.viewmodel.compose) diff --git a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/AndroidScannerViewModel.kt b/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/AndroidScannerViewModel.kt index fd97362c8..9a065a83a 100644 --- a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/AndroidScannerViewModel.kt +++ b/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/AndroidScannerViewModel.kt @@ -27,12 +27,12 @@ import org.meshtastic.core.ble.BluetoothRepository import org.meshtastic.core.datastore.RecentAddressesDataSource import org.meshtastic.core.model.RadioController import org.meshtastic.core.model.util.anonymize +import org.meshtastic.core.network.repository.UsbRepository import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.feature.connections.model.AndroidUsbDeviceData import org.meshtastic.feature.connections.model.DeviceListEntry import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase -import org.meshtastic.feature.connections.repository.UsbRepository @KoinViewModel @Suppress("LongParameterList", "TooManyFunctions") 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 5289f10c3..d620a4933 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 @@ -28,6 +28,9 @@ import org.meshtastic.core.common.database.DatabaseManager import org.meshtastic.core.datastore.RecentAddressesDataSource import org.meshtastic.core.datastore.model.RecentAddress import org.meshtastic.core.model.Node +import org.meshtastic.core.network.repository.NetworkRepository +import org.meshtastic.core.network.repository.NetworkRepository.Companion.toAddressString +import org.meshtastic.core.network.repository.UsbRepository import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.resources.Res @@ -38,9 +41,6 @@ import org.meshtastic.feature.connections.model.DeviceListEntry import org.meshtastic.feature.connections.model.DiscoveredDevices import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase import org.meshtastic.feature.connections.model.getMeshtasticShortName -import org.meshtastic.feature.connections.repository.NetworkRepository -import org.meshtastic.feature.connections.repository.NetworkRepository.Companion.toAddressString -import org.meshtastic.feature.connections.repository.UsbRepository import java.util.Locale @Suppress("LongParameterList") diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/NetworkDevices.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/NetworkDevices.kt index ce530bac7..b775b715e 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/NetworkDevices.kt +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/NetworkDevices.kt @@ -50,6 +50,7 @@ import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.meshtastic.core.common.util.isValidAddress import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.network.repository.NetworkConstants import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.add_network_device import org.meshtastic.core.resources.address @@ -60,7 +61,6 @@ import org.meshtastic.core.resources.no_network_devices_found import org.meshtastic.core.resources.recent_network_devices import org.meshtastic.feature.connections.ScannerViewModel import org.meshtastic.feature.connections.model.DeviceListEntry -import org.meshtastic.feature.connections.repository.NetworkConstants @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/mesh_service_example/src/main/kotlin/com/meshtastic/android/meshserviceexample/MainActivity.kt b/mesh_service_example/src/main/kotlin/com/meshtastic/android/meshserviceexample/MainActivity.kt index 758e9c0b3..26063e2b7 100644 --- a/mesh_service_example/src/main/kotlin/com/meshtastic/android/meshserviceexample/MainActivity.kt +++ b/mesh_service_example/src/main/kotlin/com/meshtastic/android/meshserviceexample/MainActivity.kt @@ -134,7 +134,7 @@ class MainActivity : ComponentActivity() { Log.i(TAG, "Found service in package: ${serviceInfo.packageName}") } else { Log.w(TAG, "No service found for action com.geeksville.mesh.Service. Falling back to default.") - intent.setClassName("com.geeksville.mesh", "org.meshtastic.app.service.MeshService") + intent.setClassName("com.geeksville.mesh", "org.meshtastic.core.service.MeshService") } val success = bindService(intent, serviceConnection, BIND_AUTO_CREATE) From 7d63f8b8240016e01046202cfc6dad354e8b2040 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:35:39 -0500 Subject: [PATCH 008/313] feat: build logic (#4829) Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .github/copilot-instructions.md | 14 +- .github/workflows/merge-queue.yml | 3 + .github/workflows/pull-request.yml | 74 ++++++- .github/workflows/reusable-check.yml | 197 ++++++++++++------ AGENTS.md | 14 +- GEMINI.md | 14 +- app/build.gradle.kts | 1 - build-logic/convention/build.gradle.kts | 6 +- .../main/kotlin/KmpFeatureConventionPlugin.kt | 82 ++++++++ .../meshtastic/buildlogic/FlavorResolution.kt | 19 +- .../kotlin/org/meshtastic/buildlogic/Graph.kt | 6 + core/common/build.gradle.kts | 1 - core/database/build.gradle.kts | 1 - core/di/build.gradle.kts | 7 +- core/domain/build.gradle.kts | 2 - core/network/build.gradle.kts | 8 - core/nfc/build.gradle.kts | 1 - .../meshtastic/core/prefs/di/Qualifiers.kt | 67 ------ core/ui/build.gradle.kts | 2 - docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md | 68 ++++-- docs/BUILD_LOGIC_INDEX.md | 180 +++------------- .../testing-and-ci-playbook.md | 26 ++- .../BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md | 2 +- .../BUILD_LOGIC_OPTIMIZATION_SUMMARY.md | 6 +- docs/roadmap.md | 6 +- feature/connections/build.gradle.kts | 25 +-- feature/firmware/build.gradle.kts | 16 +- feature/intro/build.gradle.kts | 19 +- feature/map/build.gradle.kts | 19 +- feature/messaging/build.gradle.kts | 22 +- feature/node/build.gradle.kts | 20 +- feature/settings/build.gradle.kts | 19 +- gradle/libs.versions.toml | 18 +- 33 files changed, 479 insertions(+), 486 deletions(-) create mode 100644 build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt delete mode 100644 core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/di/Qualifiers.kt diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 3810477f6..e828b3671 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -27,7 +27,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec | Directory | Description | | :--- | :--- | | `app/` | Main application module. Contains `MainActivity`, Koin DI modules, and app-level logic. Uses package `org.meshtastic.app`. | -| `build-logic/` | Convention plugins for shared build configuration (e.g., `meshtastic.kmp.library`, `meshtastic.koin`). | +| `build-logic/` | Convention plugins for shared build configuration (e.g., `meshtastic.kmp.feature`, `meshtastic.kmp.library`, `meshtastic.koin`). | | `config/` | Detekt static analysis rules (`config/detekt/detekt.yml`) and Spotless formatting config (`config/spotless/.editorconfig`). | | `docs/` | Architecture docs and agent playbooks. See `docs/agent-playbooks/README.md` for version baseline and task recipes. | | `core/model` | Domain models and common data structures. | @@ -50,7 +50,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec | `core/ble/` | Bluetooth Low Energy stack using Nordic libraries. | | `core/resources/` | Centralized string and image resources (Compose Multiplatform). | | `core/testing/` | **Shared test doubles, fakes, and utilities for `commonTest` across all KMP modules.** | -| `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`, `node`, `intro`, `connections`). All are KMP with `jvm()` target. | +| `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`, `node`, `intro`, `connections`). All are KMP with `jvm()` target. Use `meshtastic.kmp.feature` convention plugin. | | `desktop/` | Compose Desktop application — first non-Android KMP target. Nav 3 shell, full Koin DI graph, TCP transport with `want_config` handshake. | | `mesh_service_example/` | Sample app showing `core:api` service integration. | @@ -77,6 +77,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec - **JetBrains fork aliases:** Version catalog aliases for JetBrains-forked AndroidX artifacts use the `jetbrains-*` prefix (e.g., `jetbrains-lifecycle-runtime-compose`, `jetbrains-navigation3-ui`). Plain `androidx-*` aliases are true Google AndroidX artifacts. Never mix them up in `commonMain`. - **Room KMP:** Always use `factory = { MeshtasticDatabaseConstructor.initialize() }` in `Room.databaseBuilder` and `inMemoryDatabaseBuilder`. DAOs and Entities reside in `commonMain`. - **Testing:** Write ViewModel and business logic tests in `commonTest`. Use `core:testing` shared fakes. +- **Build-logic conventions:** In `build-logic/convention`, prefer lazy Gradle configuration (`configureEach`, `withPlugin`, provider APIs). Avoid `afterEvaluate` in convention plugins unless there is no viable lazy alternative. ### C. Namespacing - **Standard:** Use the `org.meshtastic.*` namespace for all code. @@ -116,6 +117,15 @@ Always run commands in the following order to ensure reliability. Do not attempt ``` *Note: If testing Compose UI on the JVM (Robolectric) with Java 17, pin your tests to `@Config(sdk = [34])` to avoid SDK 35 compatibility crashes.* +**CI workflow conventions (GitHub Actions):** +- Reusable CI is split into a host job and an Android matrix job in `.github/workflows/reusable-check.yml`. +- Host job runs style/static checks, explicit Android lint tasks, unit tests, and Kover XML coverage uploads once. +- Android matrix job runs explicit assemble tasks for `app` and `mesh_service_example`; instrumentation is enabled by input and matrix API. +- Prefer explicit Gradle task paths in CI (for example `app:lintFdroidDebug`, `app:connectedGoogleDebugAndroidTest`) instead of shorthand tasks like `lintDebug`. +- Pull request CI is main-only (`.github/workflows/pull-request.yml` targets `main` branch). +- Gradle cache writes are trusted on `main` and merge queue runs (`merge_group` / `gh-readonly-queue/*`); other refs use read-only cache mode in reusable CI. +- PR `check-changes` path filtering lives in `.github/workflows/pull-request.yml` and must include module dirs plus build/workflow entrypoints (`build-logic/**`, `gradle/**`, `.github/workflows/**`, `gradlew`, `settings.gradle.kts`, etc.) so CI is not skipped for infra-only changes. + ### C. Documentation Sync Update documentation continuously as part of the same change. If you modify architecture, module targets, CI tasks, validation commands, or agent workflow rules, update the relevant docs (`AGENTS.md`, `.github/copilot-instructions.md`, `GEMINI.md`, `docs/agent-playbooks/*`, `docs/kmp-status.md`, and `docs/decisions/architecture-review-2026-03.md`). diff --git a/.github/workflows/merge-queue.yml b/.github/workflows/merge-queue.yml index 06ecfa2c2..7bc267819 100644 --- a/.github/workflows/merge-queue.yml +++ b/.github/workflows/merge-queue.yml @@ -13,6 +13,9 @@ jobs: if: github.repository == 'meshtastic/Meshtastic-Android' uses: ./.github/workflows/reusable-check.yml with: + run_lint: true + run_unit_tests: true + run_instrumented_tests: true api_levels: '[26, 35]' # Comprehensive testing for Merge Queue upload_artifacts: false secrets: inherit diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 3573fdca7..a59e66500 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -2,9 +2,9 @@ name: Pull Request CI on: pull_request: - branches: [ main, develop ] + branches: [ main ] paths-ignore: - - '**.md' + - '**/*.md' - 'docs/**' - '.gitignore' @@ -26,17 +26,78 @@ jobs: with: filters: | android: + # CI/workflow implementation + - '.github/workflows/**' + - '.github/actions/**' + # Product modules validated by reusable-check - 'app/**' + - 'baselineprofile/**' + - 'desktop/**' - 'core/**' - 'feature/**' + - 'mesh_service_example/**' + # Shared build infrastructure - 'build-logic/**' + - 'config/**' + - 'gradle/**' + # Root build entrypoints/config that can alter task graph or outputs - 'build.gradle.kts' + - 'config.properties' + - 'compose_compiler_config.conf' - 'gradle.properties' + - 'gradlew' + - 'gradlew.bat' + - 'settings.gradle.kts' + - 'test.gradle.kts' + + # 1b. FILTER DRIFT CHECK: Ensures check-changes stays aligned with module roots + verify-check-changes-filter: + if: github.repository == 'meshtastic/Meshtastic-Android' && !( github.head_ref == 'scheduled-updates' || github.head_ref == 'l10n_main' ) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Verify module roots are represented in check-changes filter + run: | + python3 - <<'PY' + import re + from pathlib import Path + + settings = Path('settings.gradle.kts').read_text() + workflow = Path('.github/workflows/pull-request.yml').read_text() + + module_roots = { + module.split(':')[0] + for module in re.findall(r'":([^"]+)"', settings) + } + + allowed_extra_roots = {'baselineprofile'} + expected_roots = module_roots | allowed_extra_roots + + filter_paths = { + path.split('/')[0] + for path in re.findall(r"-\s*'([^']+/\*\*)'", workflow) + } + + actual_module_roots = filter_paths & expected_roots + + missing = sorted(expected_roots - actual_module_roots) + unexpected = sorted(actual_module_roots - expected_roots) + + if missing or unexpected: + print('check-changes filter drift detected:') + if missing: + print(' Missing roots:', ', '.join(missing)) + if unexpected: + print(' Unexpected roots:', ', '.join(unexpected)) + raise SystemExit(1) + + print('check-changes filter is aligned with settings.gradle module roots.') + PY # 2. VALIDATION & BUILD: Delegate to reusable-check.yml # We disable instrumented tests for PRs to keep feedback fast (< 10 mins). validate-and-build: - needs: check-changes + needs: [check-changes, verify-check-changes-filter] if: needs.check-changes.outputs.android == 'true' uses: ./.github/workflows/reusable-check.yml with: @@ -51,11 +112,16 @@ jobs: check-workflow-status: name: Check Workflow Status runs-on: ubuntu-latest - needs: [check-changes, validate-and-build] + needs: [check-changes, verify-check-changes-filter, validate-and-build] if: always() steps: - name: Check Workflow Status run: | + if [[ "${{ needs.verify-check-changes-filter.result }}" == "failure" || "${{ needs.verify-check-changes-filter.result }}" == "cancelled" ]]; then + echo "::error::check-changes filter verification failed" + exit 1 + fi + # If changes were detected but build failed, fail the status check if [[ "${{ needs.check-changes.outputs.android }}" == "true" && ("${{ needs.validate-and-build.result }}" == "failure" || "${{ needs.validate-and-build.result }}" == "cancelled") ]]; then echo "::error::Android Check failed" diff --git a/.github/workflows/reusable-check.yml b/.github/workflows/reusable-check.yml index 7a320582d..d9f011ad9 100644 --- a/.github/workflows/reusable-check.yml +++ b/.github/workflows/reusable-check.yml @@ -36,25 +36,22 @@ on: GRADLE_CACHE_PASSWORD: required: false +env: + DATADOG_APPLICATION_ID: ${{ secrets.DATADOG_APPLICATION_ID }} + DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }} + MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }} + GITHUB_TOKEN: ${{ github.token }} + GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }} + GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }} + GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }} + jobs: - check: + host-check: runs-on: ubuntu-latest permissions: contents: read timeout-minutes: 60 - strategy: - fail-fast: true - matrix: - api_level: ${{ fromJson(inputs.api_levels) }} - env: - DATADOG_APPLICATION_ID: ${{ secrets.DATADOG_APPLICATION_ID }} - DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }} - MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }} - GITHUB_TOKEN: ${{ github.token }} - GRADLE_CACHE_URL: ${{ secrets.GRADLE_CACHE_URL }} - GRADLE_CACHE_USERNAME: ${{ secrets.GRADLE_CACHE_USERNAME }} - GRADLE_CACHE_PASSWORD: ${{ secrets.GRADLE_CACHE_PASSWORD }} - + steps: - name: Checkout code uses: actions/checkout@v6 @@ -74,7 +71,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 with: - dependency-graph: generate-and-submit + cache-read-only: ${{ github.ref != 'refs/heads/main' && github.event_name != 'merge_group' && !startsWith(github.ref, 'refs/heads/gh-readonly-queue/') }} cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} cache-cleanup: on-success build-scan-publish: true @@ -82,34 +79,125 @@ jobs: build-scan-terms-of-use-agree: 'yes' add-job-summary: always - - name: Determine Tasks - id: tasks - run: | - IS_FIRST_API=$(echo '${{ inputs.api_levels }}' | jq -r '.[0] == ${{ matrix.api_level }}') - - # Matrix-specific tasks - TASKS="assembleDebug " - [ "${{ inputs.run_lint }}" = "true" ] && TASKS="$TASKS lintDebug " - - # Instrumented Test Tasks - if [ "${{ inputs.run_instrumented_tests }}" = "true" ]; then - TASKS="$TASKS connectedDebugAndroidTest " - fi - - echo "tasks=$TASKS" >> $GITHUB_OUTPUT - echo "is_first_api=$IS_FIRST_API" >> $GITHUB_OUTPUT - - name: Code Style & Static Analysis - if: steps.tasks.outputs.is_first_api == 'true' + if: inputs.run_lint == true run: ./gradlew spotlessCheck detekt -Pci=true --scan - - name: Shared Unit Tests - if: steps.tasks.outputs.is_first_api == 'true' && inputs.run_unit_tests == true - run: ./gradlew testDebugUnitTest testFdroidDebugUnitTest testGoogleDebugUnitTest koverXmlReport app:koverXmlReportFdroidDebug app:koverXmlReportGoogleDebug -Pci=true --continue --scan + - name: Android Lint + if: inputs.run_lint == true + run: ./gradlew app:lintFdroidDebug app:lintGoogleDebug core:barcode:lintFdroidDebug core:barcode:lintGoogleDebug core:api:lintDebug mesh_service_example:lintDebug -Pci=true --continue --scan + + - name: Shared Unit Tests & Coverage + if: inputs.run_unit_tests == true + run: ./gradlew test koverXmlReport app:koverXmlReportFdroidDebug app:koverXmlReportGoogleDebug core:api:koverXmlReportDebug core:barcode:koverXmlReportFdroidDebug core:barcode:koverXmlReportGoogleDebug mesh_service_example:koverXmlReportDebug -Pci=true --continue --scan - name: KMP JVM Smoke Compile - if: steps.tasks.outputs.is_first_api == 'true' - run: ./gradlew :core:proto:compileKotlinJvm :core:common:compileKotlinJvm :core:model:compileKotlinJvm :core:repository:compileKotlinJvm :core:di:compileKotlinJvm :core:navigation:compileKotlinJvm :core:resources:compileKotlinJvm :core:datastore:compileKotlinJvm :core:database:compileKotlinJvm :core:domain:compileKotlinJvm :core:prefs:compileKotlinJvm :core:network:compileKotlinJvm :core:data:compileKotlinJvm :core:ble:compileKotlinJvm :core:nfc:compileKotlinJvm :core:service:compileKotlinJvm :core:ui:compileKotlinJvm :feature:intro:compileKotlinJvm :feature:messaging:compileKotlinJvm :feature:connections:compileKotlinJvm :feature:map:compileKotlinJvm :feature:node:compileKotlinJvm :feature:settings:compileKotlinJvm :feature:firmware:compileKotlinJvm :desktop:test -Pci=true --continue --scan + run: ./gradlew :core:proto:compileKotlinJvm :core:common:compileKotlinJvm :core:model:compileKotlinJvm :core:repository:compileKotlinJvm :core:di:compileKotlinJvm :core:navigation:compileKotlinJvm :core:resources:compileKotlinJvm :core:datastore:compileKotlinJvm :core:database:compileKotlinJvm :core:domain:compileKotlinJvm :core:prefs:compileKotlinJvm :core:network:compileKotlinJvm :core:data:compileKotlinJvm :core:ble:compileKotlinJvm :core:nfc:compileKotlinJvm :core:service:compileKotlinJvm :core:testing:compileKotlinJvm :core:ui:compileKotlinJvm :feature:intro:compileKotlinJvm :feature:messaging:compileKotlinJvm :feature:connections:compileKotlinJvm :feature:map:compileKotlinJvm :feature:node:compileKotlinJvm :feature:settings:compileKotlinJvm :feature:firmware:compileKotlinJvm -Pci=true --continue --scan + + - name: Upload coverage results to Codecov + if: ${{ !cancelled() && inputs.run_unit_tests }} + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + slug: meshtastic/Meshtastic-Android + flags: host-unit + fail_ci_if_error: false + files: "**/build/reports/kover/report*.xml" + + - name: Upload unit test results to Codecov + if: ${{ !cancelled() && inputs.run_unit_tests }} + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + slug: meshtastic/Meshtastic-Android + flags: host-unit + fail_ci_if_error: false + report_type: test_results + files: "**/build/test-results/**/*.xml" + + - name: Upload host reports + if: ${{ always() && inputs.upload_artifacts }} + uses: actions/upload-artifact@v7 + with: + name: reports-host + path: | + **/build/reports + **/build/test-results + retention-days: 7 + + android-check: + runs-on: ubuntu-latest + permissions: + contents: read + timeout-minutes: 60 + strategy: + fail-fast: true + matrix: + api_level: ${{ fromJson(inputs.api_levels) }} + + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 + submodules: 'recursive' + + - name: Validate Gradle Wrapper + uses: gradle/actions/wrapper-validation@v5 + + - name: Set up JDK 17 + uses: actions/setup-java@v5 + with: + java-version: '17' + distribution: 'zulu' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 + with: + cache-read-only: ${{ github.ref != 'refs/heads/main' && github.event_name != 'merge_group' && !startsWith(github.ref, 'refs/heads/gh-readonly-queue/') }} + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + cache-cleanup: on-success + build-scan-publish: true + build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service' + build-scan-terms-of-use-agree: 'yes' + add-job-summary: always + + - name: Determine matrix metadata + id: matrix_meta + shell: bash + run: | + first_api=$(python3 - <<'PY' + import json + print(json.loads('${{ inputs.api_levels }}')[0]) + PY + ) + + if [[ "${{ matrix.api_level }}" == "$first_api" ]]; then + echo "is_first_api=true" >> "$GITHUB_OUTPUT" + else + echo "is_first_api=false" >> "$GITHUB_OUTPUT" + fi + + - name: Determine Android tasks + id: tasks + shell: bash + run: | + tasks=( + "app:assembleFdroidDebug" + "app:assembleGoogleDebug" + "mesh_service_example:assembleDebug" + ) + + if [[ "${{ inputs.run_instrumented_tests }}" == "true" ]]; then + tasks+=( + "app:connectedFdroidDebugAndroidTest" + "app:connectedGoogleDebugAndroidTest" + "core:barcode:connectedFdroidDebugAndroidTest" + "core:barcode:connectedGoogleDebugAndroidTest" + ) + fi + + printf 'tasks=%s\n' "${tasks[*]}" >> "$GITHUB_OUTPUT" - name: Enable KVM group perms if: inputs.run_instrumented_tests == true @@ -118,7 +206,7 @@ jobs: sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - - name: Run Flavor Check (with Emulator) + - name: Run Android Build & Instrumented Tests if: inputs.run_instrumented_tests == true uses: reactivecircus/android-emulator-runner@v2 with: @@ -127,30 +215,25 @@ jobs: force-avd-creation: false emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none disable-animations: true - script: ./gradlew ${{ steps.tasks.outputs.tasks }} -Pci=true -PenableComposeCompilerMetrics=true -PenableComposeCompilerReports=true --parallel --configuration-cache --continue --scan + script: ./gradlew ${{ steps.tasks.outputs.tasks }} -Pci=true --parallel --configuration-cache --continue --scan - - name: Run Flavor Check (no Emulator) + - name: Run Android Build if: inputs.run_instrumented_tests == false - run: ./gradlew ${{ steps.tasks.outputs.tasks }} -Pci=true -PenableComposeCompilerMetrics=true -PenableComposeCompilerReports=true --parallel --configuration-cache --continue --scan + run: ./gradlew ${{ steps.tasks.outputs.tasks }} -Pci=true --parallel --configuration-cache --continue --scan - - name: Upload coverage results to Codecov - if: ${{ !cancelled() }} + - name: Upload instrumented test results to Codecov + if: ${{ !cancelled() && inputs.run_instrumented_tests && steps.matrix_meta.outputs.is_first_api == 'true' }} uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} slug: meshtastic/Meshtastic-Android - files: "**/build/reports/kover/report*.xml" - - - name: Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} + flags: android-instrumented + fail_ci_if_error: false report_type: test_results - files: "**/build/test-results/**/*.xml,**/build/outputs/androidTest-results/**/*.xml" + files: "**/build/outputs/androidTest-results/**/*.xml" - name: Upload debug artifact - if: ${{ steps.tasks.outputs.is_first_api == 'true' && inputs.upload_artifacts }} + if: ${{ steps.matrix_meta.outputs.is_first_api == 'true' && inputs.upload_artifacts }} uses: actions/upload-artifact@v7 with: name: app-debug-apks @@ -158,20 +241,18 @@ jobs: retention-days: 14 - name: Report App Size - if: always() && steps.tasks.outputs.is_first_api == 'true' + if: ${{ always() && steps.matrix_meta.outputs.is_first_api == 'true' }} run: | echo "### 📦 App Size Report" >> $GITHUB_STEP_SUMMARY echo "| Artifact | Size |" >> $GITHUB_STEP_SUMMARY echo "| --- | --- |" >> $GITHUB_STEP_SUMMARY find app/build/outputs/apk -name "*.apk" -exec du -h {} + | awk '{print "| " $2 " | " $1 " |"}' >> $GITHUB_STEP_SUMMARY - - name: Upload reports + - name: Upload Android reports if: ${{ always() && inputs.upload_artifacts }} uses: actions/upload-artifact@v7 with: - name: reports-api-${{ matrix.api_level }} + name: reports-android-api-${{ matrix.api_level }} path: | - **/build/reports - **/build/test-results **/build/outputs/androidTest-results retention-days: 7 diff --git a/AGENTS.md b/AGENTS.md index 01f70faf7..b35b8d208 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,7 +27,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec | Directory | Description | | :--- | :--- | | `app/` | Main application module. Contains `MainActivity`, Koin DI modules, and app-level logic. Uses package `org.meshtastic.app`. | -| `build-logic/` | Convention plugins for shared build configuration (e.g., `meshtastic.kmp.library`, `meshtastic.koin`). | +| `build-logic/` | Convention plugins for shared build configuration (e.g., `meshtastic.kmp.feature`, `meshtastic.kmp.library`, `meshtastic.koin`). | | `config/` | Detekt static analysis rules (`config/detekt/detekt.yml`) and Spotless formatting config (`config/spotless/.editorconfig`). | | `docs/` | Architecture docs and agent playbooks. See `docs/agent-playbooks/README.md` for version baseline and task recipes. | | `core/model` | Domain models and common data structures. | @@ -50,7 +50,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec | `core/ble/` | Bluetooth Low Energy stack using Nordic libraries. | | `core/resources/` | Centralized string and image resources (Compose Multiplatform). | | `core/testing/` | **Shared test doubles, fakes, and utilities for `commonTest` across all KMP modules.** | -| `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`, `node`, `intro`, `connections`). All are KMP with `jvm()` target. | +| `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`, `node`, `intro`, `connections`). All are KMP with `jvm()` target. Use `meshtastic.kmp.feature` convention plugin. | | `desktop/` | Compose Desktop application — first non-Android KMP target. Nav 3 shell, full Koin DI graph, TCP transport with `want_config` handshake. | | `mesh_service_example/` | Sample app showing `core:api` service integration. | @@ -78,6 +78,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec - **Compose Multiplatform:** Version catalog aliases for Compose Multiplatform artifacts use the `compose-multiplatform-*` prefix (e.g., `compose-multiplatform-material3`, `compose-multiplatform-foundation`). Never use plain `androidx.compose` dependencies in common Main. - **Room KMP:** Always use `factory = { MeshtasticDatabaseConstructor.initialize() }` in `Room.databaseBuilder` and `inMemoryDatabaseBuilder`. DAOs and Entities reside in `commonMain`. - **Testing:** Write ViewModel and business logic tests in `commonTest`. Use `core:testing` shared fakes. +- **Build-logic conventions:** In `build-logic/convention`, prefer lazy Gradle configuration (`configureEach`, `withPlugin`, provider APIs). Avoid `afterEvaluate` in convention plugins unless there is no viable lazy alternative. ### C. Namespacing - **Standard:** Use the `org.meshtastic.*` namespace for all code. @@ -117,6 +118,15 @@ Always run commands in the following order to ensure reliability. Do not attempt ``` *Note: If testing Compose UI on the JVM (Robolectric) with Java 17, pin your tests to `@Config(sdk = [34])` to avoid SDK 35 compatibility crashes.* +**CI workflow conventions (GitHub Actions):** +- Reusable CI is split into a host job and an Android matrix job in `.github/workflows/reusable-check.yml`. +- Host job runs style/static checks, explicit Android lint tasks, unit tests, and Kover XML coverage uploads once. +- Android matrix job runs explicit assemble tasks for `app` and `mesh_service_example`; instrumentation is enabled by input and matrix API. +- Prefer explicit Gradle task paths in CI (for example `app:lintFdroidDebug`, `app:connectedGoogleDebugAndroidTest`) instead of shorthand tasks like `lintDebug`. +- Pull request CI is main-only (`.github/workflows/pull-request.yml` targets `main` branch). +- Gradle cache writes are trusted on `main` and merge queue runs (`merge_group` / `gh-readonly-queue/*`); other refs use read-only cache mode in reusable CI. +- PR `check-changes` path filtering lives in `.github/workflows/pull-request.yml` and must include module dirs plus build/workflow entrypoints (`build-logic/**`, `gradle/**`, `.github/workflows/**`, `gradlew`, `settings.gradle.kts`, etc.) so CI is not skipped for infra-only changes. + ### C. Documentation Sync Update documentation continuously as part of the same change. If you modify architecture, module targets, CI tasks, validation commands, or agent workflow rules, update the relevant docs (`AGENTS.md`, `.github/copilot-instructions.md`, `GEMINI.md`, `docs/agent-playbooks/*`, `docs/kmp-status.md`, and `docs/decisions/architecture-review-2026-03.md`). diff --git a/GEMINI.md b/GEMINI.md index 01f70faf7..b35b8d208 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -27,7 +27,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec | Directory | Description | | :--- | :--- | | `app/` | Main application module. Contains `MainActivity`, Koin DI modules, and app-level logic. Uses package `org.meshtastic.app`. | -| `build-logic/` | Convention plugins for shared build configuration (e.g., `meshtastic.kmp.library`, `meshtastic.koin`). | +| `build-logic/` | Convention plugins for shared build configuration (e.g., `meshtastic.kmp.feature`, `meshtastic.kmp.library`, `meshtastic.koin`). | | `config/` | Detekt static analysis rules (`config/detekt/detekt.yml`) and Spotless formatting config (`config/spotless/.editorconfig`). | | `docs/` | Architecture docs and agent playbooks. See `docs/agent-playbooks/README.md` for version baseline and task recipes. | | `core/model` | Domain models and common data structures. | @@ -50,7 +50,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec | `core/ble/` | Bluetooth Low Energy stack using Nordic libraries. | | `core/resources/` | Centralized string and image resources (Compose Multiplatform). | | `core/testing/` | **Shared test doubles, fakes, and utilities for `commonTest` across all KMP modules.** | -| `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`, `node`, `intro`, `connections`). All are KMP with `jvm()` target. | +| `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`, `node`, `intro`, `connections`). All are KMP with `jvm()` target. Use `meshtastic.kmp.feature` convention plugin. | | `desktop/` | Compose Desktop application — first non-Android KMP target. Nav 3 shell, full Koin DI graph, TCP transport with `want_config` handshake. | | `mesh_service_example/` | Sample app showing `core:api` service integration. | @@ -78,6 +78,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec - **Compose Multiplatform:** Version catalog aliases for Compose Multiplatform artifacts use the `compose-multiplatform-*` prefix (e.g., `compose-multiplatform-material3`, `compose-multiplatform-foundation`). Never use plain `androidx.compose` dependencies in common Main. - **Room KMP:** Always use `factory = { MeshtasticDatabaseConstructor.initialize() }` in `Room.databaseBuilder` and `inMemoryDatabaseBuilder`. DAOs and Entities reside in `commonMain`. - **Testing:** Write ViewModel and business logic tests in `commonTest`. Use `core:testing` shared fakes. +- **Build-logic conventions:** In `build-logic/convention`, prefer lazy Gradle configuration (`configureEach`, `withPlugin`, provider APIs). Avoid `afterEvaluate` in convention plugins unless there is no viable lazy alternative. ### C. Namespacing - **Standard:** Use the `org.meshtastic.*` namespace for all code. @@ -117,6 +118,15 @@ Always run commands in the following order to ensure reliability. Do not attempt ``` *Note: If testing Compose UI on the JVM (Robolectric) with Java 17, pin your tests to `@Config(sdk = [34])` to avoid SDK 35 compatibility crashes.* +**CI workflow conventions (GitHub Actions):** +- Reusable CI is split into a host job and an Android matrix job in `.github/workflows/reusable-check.yml`. +- Host job runs style/static checks, explicit Android lint tasks, unit tests, and Kover XML coverage uploads once. +- Android matrix job runs explicit assemble tasks for `app` and `mesh_service_example`; instrumentation is enabled by input and matrix API. +- Prefer explicit Gradle task paths in CI (for example `app:lintFdroidDebug`, `app:connectedGoogleDebugAndroidTest`) instead of shorthand tasks like `lintDebug`. +- Pull request CI is main-only (`.github/workflows/pull-request.yml` targets `main` branch). +- Gradle cache writes are trusted on `main` and merge queue runs (`merge_group` / `gh-readonly-queue/*`); other refs use read-only cache mode in reusable CI. +- PR `check-changes` path filtering lives in `.github/workflows/pull-request.yml` and must include module dirs plus build/workflow entrypoints (`build-logic/**`, `gradle/**`, `.github/workflows/**`, `gradlew`, `settings.gradle.kts`, etc.) so CI is not skipped for infra-only changes. + ### C. Documentation Sync Update documentation continuously as part of the same change. If you modify architecture, module targets, CI tasks, validation commands, or agent workflow rules, update the relevant docs (`AGENTS.md`, `.github/copilot-instructions.md`, `GEMINI.md`, `docs/agent-playbooks/*`, `docs/kmp-status.md`, and `docs/decisions/architecture-review-2026-03.md`). diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 60271c4c0..0b9bc8e35 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,7 +31,6 @@ plugins { alias(libs.plugins.meshtastic.android.application.compose) id("meshtastic.koin") alias(libs.plugins.kotlin.parcelize) - alias(libs.plugins.devtools.ksp) alias(libs.plugins.secrets) alias(libs.plugins.aboutlibraries) } diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 7edd78e22..31ae5278f 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -60,7 +60,6 @@ dependencies { compileOnly(libs.secrets.gradlePlugin) compileOnly(libs.spotless.gradlePlugin) compileOnly(libs.test.retry.gradlePlugin) - compileOnly(libs.truth) detektPlugins(libs.detekt.formatting) } @@ -177,6 +176,11 @@ gradlePlugin { implementationClass = "KmpLibraryComposeConventionPlugin" } + register("kmpFeature") { + id = "meshtastic.kmp.feature" + implementationClass = "KmpFeatureConventionPlugin" + } + register("dokka") { id = "meshtastic.dokka" implementationClass = "DokkaConventionPlugin" diff --git a/build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt new file mode 100644 index 000000000..b2ee6bcd3 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/KmpFeatureConventionPlugin.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2025 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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 . + */ + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.apply +import org.gradle.kotlin.dsl.configure +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.meshtastic.buildlogic.library +import org.meshtastic.buildlogic.libs + +/** + * Convention plugin for KMP feature modules. + * + * Composes [KmpLibraryConventionPlugin], [KmpLibraryComposeConventionPlugin], and + * [KoinConventionPlugin] and wires the common Compose / Lifecycle / Koin dependencies + * that every feature module needs. Feature `build.gradle.kts` files only declare + * their module-specific deps. + * + * Modelled after the `AndroidFeatureImplConventionPlugin` pattern from + * [Now in Android](https://github.com/android/nowinandroid). + */ +class KmpFeatureConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + apply(plugin = "meshtastic.kmp.library") + apply(plugin = "meshtastic.kmp.library.compose") + apply(plugin = "meshtastic.koin") + + extensions.configure { + sourceSets.getByName("commonMain").dependencies { + // Compose Multiplatform UI + implementation(libs.library("compose-multiplatform-material3")) + implementation(libs.library("compose-multiplatform-materialIconsExtended")) + + // Lifecycle & ViewModel (JetBrains KMP forks — safe in commonMain) + implementation(libs.library("jetbrains-lifecycle-viewmodel-compose")) + implementation(libs.library("jetbrains-lifecycle-runtime-compose")) + + // Koin ViewModel wiring + implementation(libs.library("koin-compose-viewmodel")) + + // Logging + implementation(libs.library("kermit")) + } + + sourceSets.getByName("androidMain").dependencies { + // Compose BOM for consistent Android Compose versions + implementation(target.dependencies.platform(libs.library("androidx-compose-bom"))) + + // Common Android Compose dependencies + implementation(libs.library("accompanist-permissions")) + implementation(libs.library("androidx-activity-compose")) + implementation(libs.library("androidx-compose-material3")) + implementation(libs.library("androidx-compose-material-iconsExtended")) + implementation(libs.library("androidx-compose-ui-text")) + implementation(libs.library("androidx-compose-ui-tooling-preview")) + } + + sourceSets.getByName("commonTest").dependencies { + implementation(project(":core:testing")) + } + } + } + } +} + + diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/FlavorResolution.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/FlavorResolution.kt index f61973b0e..620d0c830 100644 --- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/FlavorResolution.kt +++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/FlavorResolution.kt @@ -17,10 +17,11 @@ package org.meshtastic.buildlogic +import com.android.build.api.attributes.ProductFlavorAttr import org.gradle.api.Project import org.gradle.api.attributes.Attribute -private const val MARKETPLACE_ATTRIBUTE_NAME = "com.android.build.api.attributes.ProductFlavor:marketplace" +private const val LEGACY_MARKETPLACE_ATTRIBUTE_NAME = "marketplace" internal fun Project.configureAndroidMarketplaceFallback() { val defaultMarketplace = @@ -29,13 +30,16 @@ internal fun Project.configureAndroidMarketplaceFallback() { .orElse(MeshtasticFlavor.entries.first { it.default }.name) .get() - val marketplaceAttr = Attribute.of(MARKETPLACE_ATTRIBUTE_NAME, String::class.java) + val marketplaceAttr = ProductFlavorAttr.of(MeshtasticFlavor.fdroid.dimension.name) + val legacyMarketplaceAttr = Attribute.of(LEGACY_MARKETPLACE_ATTRIBUTE_NAME, String::class.java) afterEvaluate { - configurations.all { - if (!isCanBeResolved || isCanBeConsumed) return@all - if (!name.contains("android", ignoreCase = true)) return@all - if (attributes.getAttribute(marketplaceAttr) != null) return@all + configurations.configureEach { + if (!isCanBeResolved || isCanBeConsumed) return@configureEach + if (!name.contains("android", ignoreCase = true)) return@configureEach + if (attributes.getAttribute(marketplaceAttr) != null && attributes.getAttribute(legacyMarketplaceAttr) != null) { + return@configureEach + } // Prefer explicit flavor from configuration name; otherwise use configurable default. val inferredMarketplace = @@ -45,7 +49,8 @@ internal fun Project.configureAndroidMarketplaceFallback() { else -> defaultMarketplace } - attributes.attribute(marketplaceAttr, inferredMarketplace) + attributes.attribute(marketplaceAttr, objects.named(ProductFlavorAttr::class.java, inferredMarketplace)) + attributes.attribute(legacyMarketplaceAttr, inferredMarketplace) } } } diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Graph.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Graph.kt index c452daafc..9279c9419 100644 --- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Graph.kt +++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/Graph.kt @@ -79,6 +79,11 @@ internal enum class PluginType(val id: String, val ref: String, val style: Strin ref = "jvm-library", style = "fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000", ), + KmpFeature( + id = "meshtastic.kmp.feature", + ref = "kmp-feature", + style = "fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000", + ), KmpLibrary( id = "meshtastic.kmp.library", ref = "kmp-library", @@ -123,6 +128,7 @@ internal fun Project.configureGraphTasks() { val type = when { pluginManager.hasPlugin("meshtastic.android.application") || pluginManager.hasPlugin("meshtastic.android.application.compose") -> PluginType.AndroidApplication targetProjectPath.startsWith(":desktop") -> PluginType.ComposeDesktopApplication + pluginManager.hasPlugin("meshtastic.kmp.feature") -> PluginType.KmpFeature targetProjectPath.startsWith(":feature:") -> PluginType.AndroidFeature else -> PluginType.entries.firstOrNull { pluginManager.hasPlugin(it.id) } ?: Unknown } diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index b9f3826ce..f1e79df34 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -35,7 +35,6 @@ kotlin { commonMain.dependencies { api(libs.aboutlibraries.core) implementation(libs.aboutlibraries.compose.m3) - implementation(libs.javax.inject) implementation(libs.kotlinx.atomicfu) implementation(libs.kotlinx.coroutines.core) api(libs.kotlinx.datetime) diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts index 113fb0762..1815335f2 100644 --- a/core/database/build.gradle.kts +++ b/core/database/build.gradle.kts @@ -50,7 +50,6 @@ kotlin { implementation(libs.kotlinx.coroutines.test) implementation(libs.androidx.room.testing) } - androidMain.dependencies { implementation(libs.javax.inject) } val androidHostTest by getting { dependencies { diff --git a/core/di/build.gradle.kts b/core/di/build.gradle.kts index d3c8bbec9..57f4d2fd5 100644 --- a/core/di/build.gradle.kts +++ b/core/di/build.gradle.kts @@ -29,10 +29,5 @@ kotlin { androidResources.enable = false } - sourceSets { - commonMain.dependencies { - api(libs.javax.inject) - implementation(libs.kotlinx.coroutines.core) - } - } + sourceSets { commonMain.dependencies { implementation(libs.kotlinx.coroutines.core) } } } diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts index 1e3a35133..88166c417 100644 --- a/core/domain/build.gradle.kts +++ b/core/domain/build.gradle.kts @@ -17,7 +17,6 @@ plugins { alias(libs.plugins.meshtastic.kmp.library) - alias(libs.plugins.devtools.ksp) alias(libs.plugins.meshtastic.koin) } @@ -41,7 +40,6 @@ kotlin { implementation(projects.core.datastore) implementation(projects.core.resources) - api(libs.javax.inject) implementation(libs.kermit) implementation(libs.compose.multiplatform.resources) implementation(libs.okio) diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index 4fd91682f..dde171d11 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -64,11 +64,3 @@ kotlin { commonTest.dependencies { implementation(libs.kotlinx.coroutines.test) } } } - -val marketplaceAttr = Attribute.of("marketplace", String::class.java) - -configurations.all { - if (name.contains("android", ignoreCase = true)) { - attributes.attribute(marketplaceAttr, "fdroid") - } -} diff --git a/core/nfc/build.gradle.kts b/core/nfc/build.gradle.kts index fe52cea5c..559a96868 100644 --- a/core/nfc/build.gradle.kts +++ b/core/nfc/build.gradle.kts @@ -34,7 +34,6 @@ kotlin { androidMain.dependencies { implementation(libs.androidx.activity.compose) - implementation(libs.compose.multiplatform.runtime) implementation(libs.compose.multiplatform.ui) } diff --git a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/di/Qualifiers.kt b/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/di/Qualifiers.kt deleted file mode 100644 index 453ec6bc6..000000000 --- a/core/prefs/src/commonMain/kotlin/org/meshtastic/core/prefs/di/Qualifiers.kt +++ /dev/null @@ -1,67 +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.prefs.di - -import javax.inject.Qualifier - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class AnalyticsDataStore - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class HomoglyphEncodingDataStore - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class AppDataStore - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class CustomEmojiDataStore - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class MapDataStore - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class MapConsentDataStore - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class MapTileProviderDataStore - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class MeshDataStore - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class RadioDataStore - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class UiDataStore - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class MeshLogDataStore - -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class FilterDataStore diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 7171d545a..6ed7f08a8 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -48,8 +48,6 @@ kotlin { implementation(libs.compose.multiplatform.materialIconsExtended) implementation(libs.compose.multiplatform.ui) implementation(libs.compose.multiplatform.foundation) - implementation(libs.compose.multiplatform.runtime) - implementation(libs.compose.multiplatform.resources) implementation(libs.compose.multiplatform.ui.tooling) implementation(libs.kermit) diff --git a/docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md b/docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md index b70932e37..ddaa8732b 100644 --- a/docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md +++ b/docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md @@ -15,10 +15,12 @@ Quick reference for maintaining and extending the build-logic convention system. build-logic/ ├── convention/ │ ├── src/main/kotlin/ -│ │ ├── KmpLibraryConventionPlugin.kt # KMP modules: features, core -│ │ ├── KmpJvmAndroidConventionPlugin.kt # Opt-in jvmAndroidMain hierarchy for Android + desktop JVM -│ │ ├── AndroidApplicationConventionPlugin.kt # Main app -│ │ ├── AndroidLibraryConventionPlugin.kt # Android-only libraries +│ │ ├── KmpFeatureConventionPlugin.kt # KMP feature modules (composes library + compose + koin + common deps) +│ │ ├── KmpLibraryConventionPlugin.kt # KMP modules: core libraries +│ │ ├── KmpLibraryComposeConventionPlugin.kt # KMP Compose Multiplatform setup +│ │ ├── KmpJvmAndroidConventionPlugin.kt # Opt-in jvmAndroidMain hierarchy for Android + desktop JVM +│ │ ├── AndroidApplicationConventionPlugin.kt # Main app +│ │ ├── AndroidLibraryConventionPlugin.kt # Android-only libraries │ │ ├── AndroidApplicationComposeConventionPlugin.kt │ │ ├── AndroidLibraryComposeConventionPlugin.kt │ │ ├── org/meshtastic/buildlogic/ @@ -83,6 +85,48 @@ kotlin { **Why:** The convention uses Kotlin's hierarchy template API to create `jvmAndroidMain` without the `Default Kotlin Hierarchy Template Not Applied Correctly` warning triggered by hand-written `dependsOn(...)` graphs. +### Example: Creating a new KMP feature module + +**Current Pattern (GOOD ✅):** + +Use `meshtastic.kmp.feature` for any `feature:*` module. It composes `kmp.library` + `kmp.library.compose` + `koin` and provides all the common Compose/Lifecycle/Koin/Android dependencies that every feature needs: + +```kotlin +plugins { + alias(libs.plugins.meshtastic.kmp.feature) + // Optional: add only if this feature needs serialization + alias(libs.plugins.meshtastic.kotlinx.serialization) +} + +kotlin { + jvm() + android { + namespace = "org.meshtastic.feature.yourfeature" + androidResources.enable = false + withHostTest { isIncludeAndroidResources = true } + } + + sourceSets { + commonMain.dependencies { + // Only module-SPECIFIC deps here + implementation(projects.core.common) + implementation(projects.core.model) + implementation(projects.core.ui) + } + androidMain.dependencies { + // Only Android-specific extras here + } + } +} +``` + +**What the plugin provides automatically:** +- `commonMain`: `compose-multiplatform-material3`, `compose-multiplatform-materialIconsExtended`, `jetbrains-lifecycle-viewmodel-compose`, `koin-compose-viewmodel`, `kermit` +- `androidMain`: `androidx-compose-bom` (platform), `accompanist-permissions`, `androidx-activity-compose`, `androidx-compose-material3`, `androidx-compose-material-iconsExtended`, `androidx-compose-ui-text`, `androidx-compose-ui-tooling-preview` +- `commonTest`: `core:testing` + +**Why:** Eliminates ~15 duplicate dependency declarations per feature module (modelled after Now in Android's `AndroidFeatureImplConventionPlugin`). + ### Example: Adding Android-specific test config **Pattern:** Add to `AndroidLibraryConventionPlugin.kt`: @@ -228,24 +272,22 @@ extensions.configure { ### ❌ **Mistake: Side effects during configuration** ```kotlin -// WRONG: Task configuration during plugin apply (too early) +// WRONG: Eager task configuration at plugin-apply time tasks.withType { - // This runs before build.gradle.kts is parsed! + // Can realize tasks too early } -// RIGHT: Use afterEvaluate if needed -afterEvaluate { - tasks.withType { - // Runs after all configuration - } +// RIGHT: Lazy, configuration-cache-friendly wiring +tasks.withType().configureEach { + // Applies to existing and future tasks lazily } ``` ## Related Files - `AGENTS.md` - Development guidelines (Section 3.B testing, Section 4.A build protocol) -- `docs/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md` - History of optimizations +- `docs/BUILD_LOGIC_INDEX.md` - Current build-logic doc entry point (with links to active references) +- `docs/archive/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md` - Historical optimization deep-dive - `build-logic/convention/build.gradle.kts` - Convention plugin build config - `.github/copilot-instructions.md` - Build & test commands - diff --git a/docs/BUILD_LOGIC_INDEX.md b/docs/BUILD_LOGIC_INDEX.md index 20853b83f..a0cce5c50 100644 --- a/docs/BUILD_LOGIC_INDEX.md +++ b/docs/BUILD_LOGIC_INDEX.md @@ -1,165 +1,41 @@ # Build-Logic Documentation Index -Quick navigation guide for build-logic optimization and convention documentation. +Quick navigation guide for build-logic conventions in this repository. -## 📋 Start Here +## Start Here -**New to build-logic?** → `BUILD_LOGIC_CONVENTIONS_GUIDE.md` -**Want optimization details?** → `BUILD_LOGIC_OPTIMIZATION_SUMMARY.md` -**Need implementation details?** → `BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md` +- New to build-logic? -> `docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md` +- Need test-dependency specifics? -> `docs/BUILD_CONVENTION_TEST_DEPS.md` +- Need implementation code? -> `build-logic/convention/src/main/kotlin/` ---- +## Primary Docs (Current) -## 📚 Documentation Files +| Document | Purpose | +| :--- | :--- | +| `docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md` | Canonical conventions, duplication heuristics, verification commands, common pitfalls | +| `docs/BUILD_CONVENTION_TEST_DEPS.md` | Rationale and behavior for centralized KMP test dependencies | -### Executive & Strategic -| Document | Purpose | Audience | Status | -|----------|---------|----------|--------| -| **[BUILD_LOGIC_OPTIMIZATION_SUMMARY.md](BUILD_LOGIC_OPTIMIZATION_SUMMARY.md)** | High-level summary of all optimizations, completed work, and recommendations | Tech Leads, Maintainers | ✅ Final | -| **[BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md](BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md)** | Detailed analysis: what was done, why, and future opportunities | Architects, Senior Devs | ✅ Final | +## Key Conventions to Follow -### Practical & Implementation -| Document | Purpose | Audience | Status | -|----------|---------|----------|--------| -| **[BUILD_LOGIC_CONVENTIONS_GUIDE.md](BUILD_LOGIC_CONVENTIONS_GUIDE.md)** | How to maintain, extend, and follow build-logic patterns | All Developers | ✅ Reference | -| **[BUILD_CONVENTION_TEST_DEPS.md](BUILD_CONVENTION_TEST_DEPS.md)** | Specific details on test dependency centralization | Test Developers, Module Owners | ✅ Reference | +- Prefer lazy Gradle APIs in convention plugins: `configureEach`, `withPlugin`, provider APIs. +- Avoid `afterEvaluate` in `build-logic/convention` unless there is no viable lazy alternative. +- Keep convention plugins single-purpose and compose them (e.g., `meshtastic.kmp.feature` composes KMP + Compose + Koin conventions). +- Use version-catalog aliases from `gradle/libs.versions.toml` consistently. -### Analysis & Research -| Document | Purpose | Audience | Status | -|----------|---------|----------|--------| -| **[BUILD_LOGIC_OPTIMIZATION_ANALYSIS.md](BUILD_LOGIC_OPTIMIZATION_ANALYSIS.md)** | Research findings: identified issues and analysis of each | Reviewers, Curious Developers | ✅ Research | +## Verification Commands ---- - -## 🎯 Quick Links by Use Case - -### I need to... - -**Add a new test framework dependency** -1. Read: `BUILD_LOGIC_CONVENTIONS_GUIDE.md` (Section "Adding a new test framework") -2. Edit: `build-logic/.../KotlinAndroid.kt::configureKmpTestDependencies()` -3. Verify: Run `./gradlew spotlessCheck detekt test` - -**Share Java/JVM code between Android and Desktop in a KMP module** -1. Read: `BUILD_LOGIC_CONVENTIONS_GUIDE.md` (Section "Adding shared `jvmAndroidMain` code to a KMP module") -2. Apply: `id("meshtastic.kmp.jvm.android")` -3. Verify: Run `./gradlew spotlessCheck detekt assembleDebug test` - -**Understand the test dependency optimization** -1. Read: `BUILD_CONVENTION_TEST_DEPS.md` (entire file) -2. Reference: `BUILD_LOGIC_OPTIMIZATION_SUMMARY.md` (Section "Completed Optimizations") - -**Consolidate duplicate convention plugins** -1. Read: `BUILD_LOGIC_CONVENTIONS_GUIDE.md` (Section "Duplication Heuristics") -2. Reference: `BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md` (Section "Future Optimization Opportunities") -3. Review: Comments in `AndroidApplicationComposeConventionPlugin.kt` and `AndroidLibraryFlavorsConventionPlugin.kt` - -**Maintain build-logic going forward** -1. Read: `BUILD_LOGIC_CONVENTIONS_GUIDE.md` (entire file) -2. Reference: `BUILD_LOGIC_OPTIMIZATION_SUMMARY.md` (Section "Maintenance Going Forward") - -**Review optimization decisions** -1. Read: `BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md` (Section "Decision Rationale") -2. Check: Comments in modified convention plugins - ---- - -## 📊 Changes at a Glance - -### Code Changes -``` -Modified Files: 9 -Created Files: 5 (documentation) -Lines Removed: ~70 (redundant dependencies) -Lines Added: ~30 (consolidated config) - -Build Verification: -✅ spotlessCheck -✅ detekt -✅ assembleDebug -✅ test (516 tasks, all passing) +```bash +./gradlew :build-logic:convention:compileKotlin +./gradlew :build-logic:convention:validatePlugins +./gradlew spotlessCheck +./gradlew detekt ``` -### Plugin Status -``` -✅ KmpLibraryConventionPlugin - Enhanced (test deps added) -✅ AndroidApplicationCompose - Optimized (documented duplication) -✅ AndroidLibraryCompose - Optimized (documented duplication) -✅ AndroidApplicationFlavors - Optimized (documented opportunity) -✅ AndroidLibraryFlavors - Optimized (documented opportunity) -``` - ---- - -## 🔄 Historical Context - -### Previous Session (From Context) -- Identified and fixed Kotlin test compilation errors in feature modules -- Added `kotlin("test")` to individual module build files - -### This Session -- **Identified:** Opportunity to centralize test dependency configuration -- **Implemented:** Moved test dependencies to convention plugin -- **Removed:** 7 redundant dependency declarations from modules -- **Implemented:** Added `meshtastic.kmp.jvm.android` to standardize `jvmAndroidMain` hierarchy setup -- **Removed:** Manual `dependsOn(...)` wiring from `core:common`, `core:model`, `core:network`, and `core:ui` -- **Analyzed:** Composition opportunities for other duplicate plugins -- **Documented:** Future optimization paths and consolidation criteria -- **Migrated:** JetBrains Compose Multiplatform dependencies from hard-coded/legacy `compose.xyz` references to proper version catalog entries. - ---- - -## 📌 Key Decisions - -### ✅ Decision: Test Dependencies → Convention -**Result:** Deployed ✅ -**Rationale:** Large duplication (7 places), single configuration, all KMP modules benefit -**Impact:** Immediate value, easy maintenance - -### ⚠️ Decision: Keep Compose Plugins Separate -**Result:** Documented duplication ✅ -**Rationale:** Different extension types, explicit intent matters, low cost of duplication -**Future Path:** Can consolidate with `CommonExtension` if Application/Library handling diverges - -### ⚠️ Decision: Keep Flavor Plugins Separate -**Result:** Documented opportunity ✅ -**Rationale:** Different extension types, low duplication cost, Gradle conventions prefer specific types -**Future Path:** Can consolidate if flavor handling becomes more complex - ---- - -## 🚀 Next Steps - -### Immediate -- ✅ Use test dependency pattern for new modules -- ✅ Refer to guides when modifying build-logic - -### Short Term -- [ ] Consider plugin validation test suite -- [ ] Review other configuration functions for consolidation opportunities -- [ ] Investigate factoring out JetBrains CMP dependencies into `meshtastic.kmp.library.compose` convention. - -### Long Term -- [ ] Monitor if Android Application/Library handling diverges -- [ ] Revisit consolidation decisions annually -- [ ] Build optimization playbook for AI agents - ---- - -## 📞 Questions? - -- **How do test dependencies work now?** → `BUILD_CONVENTION_TEST_DEPS.md` -- **Why keep duplicate plugins?** → `BUILD_LOGIC_CONVENTIONS_GUIDE.md` (Duplication Heuristics) -- **What's planned for the future?** → `BUILD_LOGIC_OPTIMIZATION_SUMMARY.md` (Recommendations) -- **How do I add a new convention?** → `BUILD_LOGIC_CONVENTIONS_GUIDE.md` (How to Add) - ---- - -## 📝 Version Control - -**Last Updated:** March 12, 2026 -**Status:** ✅ COMPLETE AND DEPLOYED -**Test Coverage:** All changes verified with spotless, detekt, and full test suite -**Production Ready:** YES ✅ - +## Related Files +- `build-logic/convention/build.gradle.kts` +- `build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt` +- `build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/FlavorResolution.kt` +- `AGENTS.md` +- `.github/copilot-instructions.md` +- `GEMINI.md` diff --git a/docs/agent-playbooks/testing-and-ci-playbook.md b/docs/agent-playbooks/testing-and-ci-playbook.md index e0e1b2938..3832720ab 100644 --- a/docs/agent-playbooks/testing-and-ci-playbook.md +++ b/docs/agent-playbooks/testing-and-ci-playbook.md @@ -17,7 +17,7 @@ Run in this order for routine changes: Notes: - This order aligns with repository guidance in `AGENTS.md` and `.github/copilot-instructions.md`. -- CI additionally runs `testDebugUnitTest` in `.github/workflows/reusable-check.yml`. +- CI runs host verification and Android build/device verification in separate jobs inside `.github/workflows/reusable-check.yml`. ## 2) Change-type matrix @@ -53,20 +53,26 @@ Run these when relevant to map/provider/flavor-specific behavior: Current reusable check workflow includes: - `spotlessCheck detekt` -- `testDebugUnitTest testFdroidDebugUnitTest testGoogleDebugUnitTest` -- `koverXmlReport app:koverXmlReportFdroidDebug app:koverXmlReportGoogleDebug` -- JVM smoke compile (all 16 core + all 6 feature modules + `desktop:test`): - `:core:proto:compileKotlinJvm :core:common:compileKotlinJvm :core:model:compileKotlinJvm :core:repository:compileKotlinJvm :core:di:compileKotlinJvm :core:navigation:compileKotlinJvm :core:resources:compileKotlinJvm :core:datastore:compileKotlinJvm :core:database:compileKotlinJvm :core:domain:compileKotlinJvm :core:prefs:compileKotlinJvm :core:network:compileKotlinJvm :core:data:compileKotlinJvm :core:ble:compileKotlinJvm :core:service:compileKotlinJvm :core:ui:compileKotlinJvm :feature:intro:compileKotlinJvm :feature:messaging:compileKotlinJvm :feature:map:compileKotlinJvm :feature:node:compileKotlinJvm :feature:settings:compileKotlinJvm :feature:firmware:compileKotlinJvm :desktop:test` -- `assembleDebug` -- `lintDebug` -- `connectedDebugAndroidTest` (when emulator tests are enabled) +- Android lint for all directly runnable Android modules: + `app:lintFdroidDebug app:lintGoogleDebug core:barcode:lintFdroidDebug core:barcode:lintGoogleDebug core:api:lintDebug mesh_service_example:lintDebug` +- Host tests plus coverage aggregation: + `test koverXmlReport app:koverXmlReportFdroidDebug app:koverXmlReportGoogleDebug core:api:koverXmlReportDebug core:barcode:koverXmlReportFdroidDebug core:barcode:koverXmlReportGoogleDebug mesh_service_example:koverXmlReportDebug` +- JVM smoke compile for all KMP JVM targets (all compile-only modules remain explicit): + `:core:proto:compileKotlinJvm :core:common:compileKotlinJvm :core:model:compileKotlinJvm :core:repository:compileKotlinJvm :core:di:compileKotlinJvm :core:navigation:compileKotlinJvm :core:resources:compileKotlinJvm :core:datastore:compileKotlinJvm :core:database:compileKotlinJvm :core:domain:compileKotlinJvm :core:prefs:compileKotlinJvm :core:network:compileKotlinJvm :core:data:compileKotlinJvm :core:ble:compileKotlinJvm :core:nfc:compileKotlinJvm :core:service:compileKotlinJvm :core:testing:compileKotlinJvm :core:ui:compileKotlinJvm :feature:intro:compileKotlinJvm :feature:messaging:compileKotlinJvm :feature:connections:compileKotlinJvm :feature:map:compileKotlinJvm :feature:node:compileKotlinJvm :feature:settings:compileKotlinJvm :feature:firmware:compileKotlinJvm` +- Android build tasks: + `app:assembleFdroidDebug app:assembleGoogleDebug mesh_service_example:assembleDebug` +- Instrumented tests (when emulator tests are enabled): + `app:connectedFdroidDebugAndroidTest app:connectedGoogleDebugAndroidTest core:barcode:connectedFdroidDebugAndroidTest core:barcode:connectedGoogleDebugAndroidTest` +- Coverage uploads happen once from the host job; instrumented test results upload once from the first Android matrix API to avoid duplicate reporting. Reference: `.github/workflows/reusable-check.yml` PR workflow note: -- `.github/workflows/pull-request.yml` ignores docs-only changes (`**.md`, `docs/**`), so doc-only PRs may skip Android CI by design. -- Android CI on PRs runs with `run_instrumented_tests: false`; emulator tests are handled in other workflow contexts. +- `.github/workflows/pull-request.yml` ignores docs-only changes (`**/*.md`, `docs/**`), so doc-only PRs may skip Android CI by design. +- PR change detection includes workflow/build/config paths such as `.github/workflows/**`, `desktop/**`, `mesh_service_example/**`, `config/**`, `gradle/**`, `settings.gradle.kts`, and `test.gradle.kts`. +- Android CI on PRs runs with `run_instrumented_tests: false`; merge queue keeps the full emulator matrix on API 26 and 35. +- Gradle cache writes are enabled for trusted refs/events (`main`, `merge_group`, and `gh-readonly-queue/*`); other refs run in read-only cache mode. ## 5) Practical guidance for agents diff --git a/docs/archive/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md b/docs/archive/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md index 8903978e8..769119dea 100644 --- a/docs/archive/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md +++ b/docs/archive/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md @@ -227,7 +227,7 @@ Add unit tests to `build-logic` verifying: ## Related Documentation - `docs/BUILD_CONVENTION_TEST_DEPS.md` - Details on test dependency centralization -- `docs/BUILD_LOGIC_OPTIMIZATION_ANALYSIS.md` - Full analysis of optimization opportunities +- `docs/archive/BUILD_LOGIC_OPTIMIZATION_ANALYSIS.md` - Full analysis of optimization opportunities - `AGENTS.md` - Updated testing + KMP hierarchy guidelines (Section 3.B) diff --git a/docs/archive/BUILD_LOGIC_OPTIMIZATION_SUMMARY.md b/docs/archive/BUILD_LOGIC_OPTIMIZATION_SUMMARY.md index a4dae61f5..deaabf95a 100644 --- a/docs/archive/BUILD_LOGIC_OPTIMIZATION_SUMMARY.md +++ b/docs/archive/BUILD_LOGIC_OPTIMIZATION_SUMMARY.md @@ -109,13 +109,13 @@ AFTER: - Summary of changes and impact - Benefits for module developers -### 2. `docs/BUILD_LOGIC_OPTIMIZATION_ANALYSIS.md` +### 2. `docs/archive/BUILD_LOGIC_OPTIMIZATION_ANALYSIS.md` - Complete analysis of 4 optimization opportunities - High/Medium/Low priority classification - Implementation cost/benefit analysis - Future recommendations -### 3. `docs/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md` ⭐ PRIMARY REFERENCE +### 3. `docs/archive/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md` ⭐ PRIMARY REFERENCE - Full summary of all optimizations - Build-logic plugin inventory with duplication status - Future opportunities with effort estimates @@ -263,7 +263,7 @@ AFTER: 1 opt-in convention plugin ### For Developers - Use `docs/BUILD_LOGIC_CONVENTIONS_GUIDE.md` when modifying build-logic - Follow test dependency patterns when creating new KMP modules -- Reference `docs/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md` for consolidation opportunities +- Reference `docs/archive/BUILD_LOGIC_OPTIMIZATIONS_COMPLETE.md` for consolidation opportunities ### For Code Reviewers - Watch for duplicate convention plugins (can consolidate if appropriate) diff --git a/docs/roadmap.md b/docs/roadmap.md index 630984bc6..01fb9402e 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap -> Last updated: 2026-03-16 +> Last updated: 2026-03-17 Forward-looking priorities for the Meshtastic KMP multi-target effort. For current state, see [`kmp-status.md`](./kmp-status.md). For the full gap analysis, see [`decisions/architecture-review-2026-03.md`](./decisions/architecture-review-2026-03.md). @@ -16,7 +16,7 @@ These items address structural gaps identified in the March 2026 architecture re | Add feature module `commonTest` (settings, node, messaging) | Medium | Medium | ✅ | | Desktop Koin `checkModules()` integration test | Medium | Low | ✅ | | Auto-wire Desktop ViewModels via K2 Compiler (eliminate manual wiring) | Medium | Low | ✅ | -| **Migrate to JetBrains Compose Multiplatform dependencies** | High | Low | ✅ | +here| **Migrate to JetBrains Compose Multiplatform dependencies** | High | Low | ✅ | ## Active Work @@ -81,7 +81,7 @@ These items address structural gaps identified in the March 2026 architecture re 4. **`feature:connections` module** — ✅ Done: Extracted connections UI into KMP feature module with dynamic transport availability detection 5. **Navigation 3 parity baseline** — ✅ Done: shared `TopLevelDestination` in `core:navigation`; both shells use same enum; parity tests in `core:navigation/commonTest` and `desktop/test` 6. **iOS CI gate** — add `iosArm64()`/`iosSimulatorArm64()` to convention plugins and CI (compile-only, no implementations) -7. **Build-logic consolidation** — **Planned:** Consolidate expansive build-logic convention plugins. There is currently some duplication in Compose dependencies that should be factored into common conventions (`meshtastic.kmp.library.compose` vs manually specifying JetBrains CMP deps in feature modules). +7. **Build-logic consolidation** — ✅ Done: Created `meshtastic.kmp.feature` convention plugin (modelled after NiA's `AndroidFeatureImplConventionPlugin`). Composes `kmp.library` + `kmp.library.compose` + `koin` and wires common Compose/Lifecycle/Koin/androidMain deps. All 7 feature modules migrated; ~100 duplicated dep lines eliminated. ## Medium-Term Priorities (60 days) diff --git a/feature/connections/build.gradle.kts b/feature/connections/build.gradle.kts index 292ebfa15..2688ed521 100644 --- a/feature/connections/build.gradle.kts +++ b/feature/connections/build.gradle.kts @@ -15,11 +15,7 @@ * along with this program. If not, see . */ -plugins { - alias(libs.plugins.meshtastic.kmp.library) - alias(libs.plugins.meshtastic.kmp.library.compose) - alias(libs.plugins.meshtastic.koin) -} +plugins { alias(libs.plugins.meshtastic.kmp.feature) } kotlin { jvm() @@ -33,8 +29,6 @@ kotlin { sourceSets { commonMain.dependencies { - implementation(libs.compose.multiplatform.material3) - implementation(libs.compose.multiplatform.materialIconsExtended) implementation(libs.compose.multiplatform.foundation) implementation(projects.core.common) implementation(projects.core.data) @@ -53,25 +47,10 @@ kotlin { implementation(projects.core.network) implementation(projects.feature.settings) - implementation(libs.jetbrains.lifecycle.viewmodel.compose) implementation(libs.jetbrains.navigation3.runtime) - implementation(libs.koin.compose.viewmodel) - implementation(libs.kermit) } - androidMain.dependencies { - implementation(project.dependencies.platform(libs.androidx.compose.bom)) - implementation(libs.accompanist.permissions) - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.material.iconsExtended) - implementation(libs.androidx.compose.ui.text) - implementation(libs.androidx.compose.ui.tooling.preview) - implementation(libs.jetbrains.lifecycle.runtime.compose) - implementation(libs.usb.serial.android) - } - - commonTest.dependencies { implementation(projects.core.testing) } + androidMain.dependencies { implementation(libs.usb.serial.android) } androidUnitTest.dependencies { implementation(libs.mockk) diff --git a/feature/firmware/build.gradle.kts b/feature/firmware/build.gradle.kts index 69a1c3fc7..582048d64 100644 --- a/feature/firmware/build.gradle.kts +++ b/feature/firmware/build.gradle.kts @@ -16,10 +16,8 @@ */ plugins { - alias(libs.plugins.meshtastic.kmp.library) - alias(libs.plugins.meshtastic.kmp.library.compose) + alias(libs.plugins.meshtastic.kmp.feature) alias(libs.plugins.meshtastic.kotlinx.serialization) - alias(libs.plugins.meshtastic.koin) } kotlin { @@ -49,22 +47,12 @@ kotlin { implementation(projects.core.ui) implementation(libs.kable.core) - implementation(libs.jetbrains.lifecycle.viewmodel.compose) - implementation(libs.koin.compose.viewmodel) - implementation(libs.kermit) implementation(libs.kotlinx.collections.immutable) implementation(libs.ktor.client.core) } androidMain.dependencies { - implementation(project.dependencies.platform(libs.androidx.compose.bom)) - implementation(libs.accompanist.permissions) - implementation(libs.androidx.activity.compose) implementation(libs.androidx.appcompat) - implementation(libs.androidx.compose.material.iconsExtended) - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.ui.text) - implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.nordic.dfu) implementation(libs.coil) implementation(libs.coil.network.okhttp) @@ -73,8 +61,6 @@ kotlin { implementation(libs.markdown.renderer) } - commonTest.dependencies { implementation(projects.core.testing) } - val androidHostTest by getting { dependencies { implementation(libs.junit) diff --git a/feature/intro/build.gradle.kts b/feature/intro/build.gradle.kts index 4b26bd1c3..4cb6ea2a6 100644 --- a/feature/intro/build.gradle.kts +++ b/feature/intro/build.gradle.kts @@ -16,10 +16,8 @@ */ plugins { - alias(libs.plugins.meshtastic.kmp.library) - alias(libs.plugins.meshtastic.kmp.library.compose) + alias(libs.plugins.meshtastic.kmp.feature) alias(libs.plugins.meshtastic.kotlinx.serialization) - alias(libs.plugins.meshtastic.koin) } kotlin { @@ -40,23 +38,10 @@ kotlin { implementation(projects.core.ui) implementation(projects.core.resources) - implementation(libs.jetbrains.lifecycle.viewmodel.compose) - implementation(libs.koin.compose.viewmodel) implementation(libs.jetbrains.navigation3.runtime) } - androidMain.dependencies { - implementation(project.dependencies.platform(libs.androidx.compose.bom)) - implementation(libs.accompanist.permissions) - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.material.iconsExtended) - implementation(libs.androidx.compose.ui.text) - implementation(libs.androidx.compose.ui.tooling.preview) - implementation(libs.jetbrains.navigation3.ui) - } - - commonTest.dependencies { implementation(projects.core.testing) } + androidMain.dependencies { implementation(libs.jetbrains.navigation3.ui) } androidUnitTest.dependencies { implementation(libs.junit) diff --git a/feature/map/build.gradle.kts b/feature/map/build.gradle.kts index c87dc492f..96378e519 100644 --- a/feature/map/build.gradle.kts +++ b/feature/map/build.gradle.kts @@ -15,10 +15,8 @@ * along with this program. If not, see . */ plugins { - alias(libs.plugins.meshtastic.kmp.library) - alias(libs.plugins.meshtastic.kmp.library.compose) + alias(libs.plugins.meshtastic.kmp.feature) alias(libs.plugins.meshtastic.kotlinx.serialization) - alias(libs.plugins.meshtastic.koin) } kotlin { @@ -45,34 +43,19 @@ kotlin { implementation(projects.core.resources) implementation(projects.core.ui) implementation(projects.core.di) - - implementation(libs.jetbrains.lifecycle.viewmodel.compose) - implementation(libs.koin.compose.viewmodel) } androidMain.dependencies { - implementation(project.dependencies.platform(libs.androidx.compose.bom)) implementation(libs.androidx.datastore) implementation(libs.androidx.datastore.preferences) - implementation(libs.accompanist.permissions) - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.annotation) - implementation(libs.androidx.compose.material.iconsExtended) - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.ui.text) - implementation(libs.androidx.compose.ui.tooling.preview) - implementation(libs.jetbrains.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.androidx.navigation.common) implementation(libs.androidx.savedstate.compose) implementation(libs.androidx.savedstate.ktx) implementation(libs.material) - implementation(libs.kermit) } - commonTest.dependencies { implementation(projects.core.testing) } - androidUnitTest.dependencies { implementation(libs.junit) implementation(libs.mockk) diff --git a/feature/messaging/build.gradle.kts b/feature/messaging/build.gradle.kts index 51f68a61c..41acdc078 100644 --- a/feature/messaging/build.gradle.kts +++ b/feature/messaging/build.gradle.kts @@ -15,11 +15,7 @@ * along with this program. If not, see . */ -plugins { - alias(libs.plugins.meshtastic.kmp.library) - alias(libs.plugins.meshtastic.kmp.library.compose) - alias(libs.plugins.meshtastic.koin) -} +plugins { alias(libs.plugins.meshtastic.kmp.feature) } kotlin { jvm() @@ -33,8 +29,6 @@ kotlin { sourceSets { commonMain.dependencies { - implementation(libs.compose.multiplatform.material3) - implementation(libs.compose.multiplatform.materialIconsExtended) implementation(libs.compose.multiplatform.foundation) implementation(projects.core.common) implementation(projects.core.data) @@ -48,10 +42,7 @@ kotlin { implementation(projects.core.service) implementation(projects.core.ui) - implementation(libs.jetbrains.lifecycle.viewmodel.compose) implementation(libs.jetbrains.navigation3.runtime) - implementation(libs.koin.compose.viewmodel) - implementation(libs.kermit) implementation(libs.androidx.paging.common) // JetBrains Material 3 Adaptive (multiplatform ListDetailPaneScaffold) @@ -61,21 +52,10 @@ kotlin { } androidMain.dependencies { - implementation(project.dependencies.platform(libs.androidx.compose.bom)) - implementation(libs.accompanist.permissions) - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.material.iconsExtended) - implementation(libs.androidx.compose.ui.text) - implementation(libs.androidx.compose.ui.tooling.preview) - implementation(libs.jetbrains.lifecycle.runtime.compose) - implementation(libs.androidx.paging.compose) implementation(libs.androidx.work.runtime.ktx) } - commonTest.dependencies { implementation(projects.core.testing) } - androidUnitTest.dependencies { implementation(libs.mockk) implementation(libs.androidx.work.testing) diff --git a/feature/node/build.gradle.kts b/feature/node/build.gradle.kts index 7ac8b750e..d59704a65 100644 --- a/feature/node/build.gradle.kts +++ b/feature/node/build.gradle.kts @@ -16,10 +16,8 @@ */ plugins { - alias(libs.plugins.meshtastic.kmp.library) - alias(libs.plugins.meshtastic.kmp.library.compose) + alias(libs.plugins.meshtastic.kmp.feature) alias(libs.plugins.meshtastic.kotlinx.serialization) - alias(libs.plugins.meshtastic.koin) } kotlin { @@ -34,8 +32,6 @@ kotlin { sourceSets { commonMain.dependencies { - implementation(libs.compose.multiplatform.material3) - implementation(libs.compose.multiplatform.materialIconsExtended) implementation(libs.coil) implementation(projects.core.common) implementation(projects.core.data) @@ -52,11 +48,7 @@ kotlin { implementation(projects.core.di) implementation(projects.feature.map) - implementation(libs.jetbrains.lifecycle.runtime.compose) - implementation(libs.jetbrains.lifecycle.viewmodel.compose) implementation(libs.jetbrains.navigation3.runtime) - implementation(libs.koin.compose.viewmodel) - implementation(libs.kermit) implementation(libs.kotlinx.collections.immutable) implementation(libs.markdown.renderer) implementation(libs.markdown.renderer.m3) @@ -71,15 +63,7 @@ kotlin { } androidMain.dependencies { - implementation(project.dependencies.platform(libs.androidx.compose.bom)) - - implementation(libs.accompanist.permissions) - implementation(libs.androidx.activity.compose) implementation(libs.androidx.appcompat) - implementation(libs.androidx.compose.material.iconsExtended) - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.ui.text) - implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.coil) implementation(libs.markdown.renderer.android) @@ -87,8 +71,6 @@ kotlin { implementation(libs.markdown.renderer) } - commonTest.dependencies { implementation(projects.core.testing) } - androidUnitTest.dependencies { implementation(libs.junit) implementation(libs.mockk) diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index 916fe7b53..66d0e2245 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -16,10 +16,8 @@ */ plugins { - alias(libs.plugins.meshtastic.kmp.library) - alias(libs.plugins.meshtastic.kmp.library.compose) + alias(libs.plugins.meshtastic.kmp.feature) alias(libs.plugins.meshtastic.kotlinx.serialization) - alias(libs.plugins.meshtastic.koin) } kotlin { @@ -33,8 +31,6 @@ kotlin { sourceSets { commonMain.dependencies { - implementation(libs.compose.multiplatform.material3) - implementation(libs.compose.multiplatform.materialIconsExtended) implementation(projects.core.common) implementation(projects.core.data) implementation(projects.core.database) @@ -49,10 +45,6 @@ kotlin { implementation(projects.core.ui) implementation(projects.core.di) - implementation(libs.jetbrains.lifecycle.viewmodel.compose) - implementation(libs.jetbrains.lifecycle.runtime.compose) - implementation(libs.koin.compose.viewmodel) - implementation(libs.kermit) implementation(libs.kotlinx.collections.immutable) implementation(libs.aboutlibraries.compose.m3) } @@ -60,14 +52,7 @@ kotlin { androidMain.dependencies { implementation(projects.core.barcode) implementation(projects.core.nfc) - implementation(project.dependencies.platform(libs.androidx.compose.bom)) - implementation(libs.accompanist.permissions) - implementation(libs.androidx.activity.compose) implementation(libs.androidx.appcompat) - implementation(libs.androidx.compose.material.iconsExtended) - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.ui.text) - implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.coil) implementation(libs.markdown.renderer.android) @@ -75,8 +60,6 @@ kotlin { implementation(libs.markdown.renderer) } - commonTest.dependencies { implementation(projects.core.testing) } - androidUnitTest.dependencies { implementation(libs.junit) implementation(libs.mockk) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 186e3b869..d4e00db08 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -49,10 +49,13 @@ ktor = "3.4.1" # Other aboutlibraries = "13.2.1" coil = "3.4.0" +datadog-gradle = "1.24.0" dd-sdk-android = "3.7.1" detekt = "1.23.8" dokka = "2.2.0-Beta" devtools-ksp = "2.3.6" +firebase-crashlytics-gradle = "3.0.6" +google-services-gradle = "4.4.4" markdownRenderer = "0.39.2" okio = "3.17.0" osmdroid-android = "6.1.20" @@ -159,7 +162,6 @@ mlkit-barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version play-services-maps = { module = "com.google.android.gms:play-services-maps", version = "20.0.0" } wire-runtime = { module = "com.squareup.wire:wire-runtime", version.ref = "wire" } zxing-core = { module = "com.google.zxing:core", version = "3.5.4" } -truth = { module = "com.google.truth:truth", version = "1.4.5" } # Jetbrains kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } @@ -208,7 +210,6 @@ dd-sdk-android-timber = { module = "com.datadoghq:dd-sdk-android-timber", versio dd-sdk-android-trace = { module = "com.datadoghq:dd-sdk-android-trace", version.ref = "dd-sdk-android" } dd-sdk-android-trace-otel = { module = "com.datadoghq:dd-sdk-android-trace-otel", version.ref = "dd-sdk-android" } dokka-android-documentation-plugin = { module = "org.jetbrains.dokka:android-documentation-plugin", version.ref = "dokka" } -javax-inject = { module = "javax.inject:javax.inject", version = "1" } markdown-renderer = { module = "com.mikepenz:multiplatform-markdown-renderer", version.ref = "markdownRenderer" } markdown-renderer-m3 = { module = "com.mikepenz:multiplatform-markdown-renderer-m3", version.ref = "markdownRenderer" } markdown-renderer-android = { module = "com.mikepenz:multiplatform-markdown-renderer-android", version.ref = "markdownRenderer" } @@ -235,12 +236,12 @@ android-tools-common = { module = "com.android.tools:common", version = "32.1.0" androidx-room-gradlePlugin = { module = "androidx.room:room-gradle-plugin", version.ref = "room" } compose-gradlePlugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" } compose-multiplatform-gradlePlugin = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "compose-multiplatform" } -datadog-gradlePlugin = { module = "com.datadoghq.dd-sdk-android-gradle-plugin:com.datadoghq.dd-sdk-android-gradle-plugin.gradle.plugin", version = "1.24.0" } +datadog-gradlePlugin = { module = "com.datadoghq.dd-sdk-android-gradle-plugin:com.datadoghq.dd-sdk-android-gradle-plugin.gradle.plugin", version.ref = "datadog-gradle" } detekt-compose = { module = "io.nlopez.compose.rules:detekt", version = "0.5.6" } detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } detekt-gradlePlugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } -firebase-crashlytics-gradlePlugin = { module = "com.google.firebase:firebase-crashlytics-gradle", version = "3.0.6" } -google-services-gradlePlugin = { module = "com.google.gms.google-services:com.google.gms.google-services.gradle.plugin", version = "4.4.4" } +firebase-crashlytics-gradlePlugin = { module = "com.google.firebase:firebase-crashlytics-gradle", version.ref = "firebase-crashlytics-gradle" } +google-services-gradlePlugin = { module = "com.google.gms.google-services:com.google.gms.google-services.gradle.plugin", version.ref = "google-services-gradle" } koin-gradlePlugin = { module = "io.insert-koin.compiler.plugin:io.insert-koin.compiler.plugin.gradle.plugin", version.ref = "koin-plugin" } kover-gradlePlugin = { module = "org.jetbrains.kotlinx.kover:org.jetbrains.kotlinx.kover.gradle.plugin", version.ref = "kover" } ksp-gradlePlugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "devtools-ksp" } @@ -267,16 +268,16 @@ kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } # Google devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "devtools-ksp" } -google-services = { id = "com.google.gms.google-services", version = "4.4.4" } +google-services = { id = "com.google.gms.google-services", version.ref = "google-services-gradle" } secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version = "2.0.1" } # Firebase -firebase-crashlytics = { id = "com.google.firebase.crashlytics", version = "3.0.6" } +firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebase-crashlytics-gradle" } firebase-perf = { id = "com.google.firebase.firebase-perf", version = "2.0.2" } # Other aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlibraries" } -datadog = { id = "com.datadoghq.dd-sdk-android-gradle-plugin", version = "1.24.0" } +datadog = { id = "com.datadoghq.dd-sdk-android-gradle-plugin", version.ref = "datadog-gradle" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } wire = { id = "com.squareup.wire", version.ref = "wire" } @@ -299,6 +300,7 @@ meshtastic-android-test = { id = "meshtastic.android.test" } meshtastic-detekt = { id = "meshtastic.detekt" } meshtastic-koin = { id = "meshtastic.koin" } meshtastic-kotlinx-serialization = { id = "meshtastic.kotlinx.serialization" } +meshtastic-kmp-feature = { id = "meshtastic.kmp.feature" } meshtastic-kmp-library = { id = "meshtastic.kmp.library" } meshtastic-kmp-library-compose = { id = "meshtastic.kmp.library.compose" } meshtastic-root = { id = "meshtastic.root" } From afa75521411e7d34079b61cdcf7c4deafad6cbe1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:39:05 -0500 Subject: [PATCH 009/313] chore(deps): update koin.plugin to v0.4.1 (#4763) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d4e00db08..388620382 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ room = "2.8.4" savedstate = "1.4.0" koin = "4.2.0" koin-annotations = "2.1.0" -koin-plugin = "0.4.0" +koin-plugin = "0.4.1" # Kotlin kotlin = "2.3.20" From 3bbb8a65ba68c630053e4bcf885adc33066a6962 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:39:48 -0500 Subject: [PATCH 010/313] chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4831) --- app/src/main/assets/firmware_releases.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/assets/firmware_releases.json b/app/src/main/assets/firmware_releases.json index 6e1d9c702..16680c478 100644 --- a/app/src/main/assets/firmware_releases.json +++ b/app/src/main/assets/firmware_releases.json @@ -188,6 +188,12 @@ ] }, "pullRequests": [ + { + "id": "9931", + "title": "fix: apply LoRa config changes live without rebooting", + "page_url": "https://github.com/meshtastic/firmware/pull/9931", + "zip_url": "https://discord.com/invite/meshtastic" + }, { "id": "9916", "title": "Fix for preserving pki_encrypted and public_key when relaying UDP multicast packets to radio.", From cb95cace25b74ded2b06b0ee3d13d1a6b82f7354 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Tue, 17 Mar 2026 16:51:09 -0500 Subject: [PATCH 011/313] chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4832) --- app/README.md | 1 + core/api/README.md | 1 + core/barcode/README.md | 1 + core/ble/README.md | 1 + core/common/README.md | 1 + core/data/README.md | 1 + core/database/README.md | 1 + core/datastore/README.md | 1 + core/di/README.md | 1 + core/model/README.md | 1 + core/navigation/README.md | 1 + core/network/README.md | 1 + core/nfc/README.md | 1 + core/prefs/README.md | 1 + core/proto/README.md | 1 + core/resources/README.md | 1 + core/service/README.md | 1 + core/ui/README.md | 1 + feature/firmware/README.md | 3 ++- feature/intro/README.md | 3 ++- feature/map/README.md | 3 ++- feature/messaging/README.md | 3 ++- feature/node/README.md | 3 ++- feature/settings/README.md | 3 ++- 24 files changed, 30 insertions(+), 6 deletions(-) diff --git a/app/README.md b/app/README.md index 85defa751..18f5ddac3 100644 --- a/app/README.md +++ b/app/README.md @@ -58,6 +58,7 @@ classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; diff --git a/core/api/README.md b/core/api/README.md index c7e64000a..1a8f10f02 100644 --- a/core/api/README.md +++ b/core/api/README.md @@ -60,6 +60,7 @@ classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; diff --git a/core/barcode/README.md b/core/barcode/README.md index 076b6a503..ebbaf06f9 100644 --- a/core/barcode/README.md +++ b/core/barcode/README.md @@ -54,6 +54,7 @@ classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; diff --git a/core/ble/README.md b/core/ble/README.md index 1ade19974..90cb7f3f2 100644 --- a/core/ble/README.md +++ b/core/ble/README.md @@ -15,6 +15,7 @@ classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; diff --git a/core/common/README.md b/core/common/README.md index a98a2a4eb..e68323fa6 100644 --- a/core/common/README.md +++ b/core/common/README.md @@ -32,6 +32,7 @@ classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; diff --git a/core/data/README.md b/core/data/README.md index b575605f8..b30b59f3b 100644 --- a/core/data/README.md +++ b/core/data/README.md @@ -28,6 +28,7 @@ classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; diff --git a/core/database/README.md b/core/database/README.md index 3323d6b96..873fdd394 100644 --- a/core/database/README.md +++ b/core/database/README.md @@ -35,6 +35,7 @@ classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; diff --git a/core/datastore/README.md b/core/datastore/README.md index 4d2605a11..931d680d5 100644 --- a/core/datastore/README.md +++ b/core/datastore/README.md @@ -28,6 +28,7 @@ classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; diff --git a/core/di/README.md b/core/di/README.md index c0bf3bfd4..40481d3cb 100644 --- a/core/di/README.md +++ b/core/di/README.md @@ -29,6 +29,7 @@ classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; diff --git a/core/model/README.md b/core/model/README.md index 40ae52961..9521c445f 100644 --- a/core/model/README.md +++ b/core/model/README.md @@ -41,6 +41,7 @@ classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; diff --git a/core/navigation/README.md b/core/navigation/README.md index 5f5e91292..00951f30e 100644 --- a/core/navigation/README.md +++ b/core/navigation/README.md @@ -36,6 +36,7 @@ classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; diff --git a/core/network/README.md b/core/network/README.md index 755e49e4d..0d7649343 100644 --- a/core/network/README.md +++ b/core/network/README.md @@ -27,6 +27,7 @@ classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; diff --git a/core/nfc/README.md b/core/nfc/README.md index 745f58b08..8a5df3c59 100644 --- a/core/nfc/README.md +++ b/core/nfc/README.md @@ -26,6 +26,7 @@ classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; diff --git a/core/prefs/README.md b/core/prefs/README.md index 4061f1818..d9fbe8f5e 100644 --- a/core/prefs/README.md +++ b/core/prefs/README.md @@ -28,6 +28,7 @@ classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; diff --git a/core/proto/README.md b/core/proto/README.md index 7c92fbaa7..aedb7ac34 100644 --- a/core/proto/README.md +++ b/core/proto/README.md @@ -31,6 +31,7 @@ classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; diff --git a/core/resources/README.md b/core/resources/README.md index c01dd900f..0528e762c 100644 --- a/core/resources/README.md +++ b/core/resources/README.md @@ -34,6 +34,7 @@ classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; diff --git a/core/service/README.md b/core/service/README.md index b7daa4047..c889b3d90 100644 --- a/core/service/README.md +++ b/core/service/README.md @@ -32,6 +32,7 @@ classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; diff --git a/core/ui/README.md b/core/ui/README.md index d732c13b1..f660cb942 100644 --- a/core/ui/README.md +++ b/core/ui/README.md @@ -59,6 +59,7 @@ classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; diff --git a/feature/firmware/README.md b/feature/firmware/README.md index 349826b2a..19e5e6a71 100644 --- a/feature/firmware/README.md +++ b/feature/firmware/README.md @@ -5,7 +5,7 @@ ```mermaid graph TB - :feature:firmware[firmware]:::android-feature + :feature:firmware[firmware]:::kmp-feature classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; @@ -15,6 +15,7 @@ classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; diff --git a/feature/intro/README.md b/feature/intro/README.md index 50376415f..a9215fd76 100644 --- a/feature/intro/README.md +++ b/feature/intro/README.md @@ -19,7 +19,7 @@ Dedicated screens for explaining and requesting specific permissions: ```mermaid graph TB - :feature:intro[intro]:::android-feature + :feature:intro[intro]:::kmp-feature classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; @@ -29,6 +29,7 @@ classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; diff --git a/feature/map/README.md b/feature/map/README.md index f3bd8189b..e2791d299 100644 --- a/feature/map/README.md +++ b/feature/map/README.md @@ -26,7 +26,7 @@ The base logic for managing map state, node markers, and camera positions. ```mermaid graph TB - :feature:map[map]:::android-feature + :feature:map[map]:::kmp-feature classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; @@ -36,6 +36,7 @@ classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; diff --git a/feature/messaging/README.md b/feature/messaging/README.md index 02622d09f..3999d07bd 100644 --- a/feature/messaging/README.md +++ b/feature/messaging/README.md @@ -25,7 +25,7 @@ A security-focused utility that detects and transforms homoglyphs (visually simi ```mermaid graph TB - :feature:messaging[messaging]:::android-feature + :feature:messaging[messaging]:::kmp-feature classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; @@ -35,6 +35,7 @@ classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; diff --git a/feature/node/README.md b/feature/node/README.md index e33ead1ea..8d53b284f 100644 --- a/feature/node/README.md +++ b/feature/node/README.md @@ -22,7 +22,7 @@ Provides a compass interface to show the relative direction and distance to othe ```mermaid graph TB - :feature:node[node]:::android-feature + :feature:node[node]:::kmp-feature classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; @@ -32,6 +32,7 @@ classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; diff --git a/feature/settings/README.md b/feature/settings/README.md index ba977f7fc..10b7ae14d 100644 --- a/feature/settings/README.md +++ b/feature/settings/README.md @@ -24,7 +24,7 @@ Displays version information, licenses, and project links. ```mermaid graph TB - :feature:settings[settings]:::android-feature + :feature:settings[settings]:::kmp-feature classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; classDef android-application-compose fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000; @@ -34,6 +34,7 @@ classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-library-compose fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000; classDef android-test fill:#A0C4FF,stroke:#000,stroke-width:2px,color:#000; classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000; +classDef kmp-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000; classDef kmp-library fill:#FFC1CC,stroke:#000,stroke-width:2px,color:#000; classDef unknown fill:#FFADAD,stroke:#000,stroke-width:2px,color:#000; From 49a6a1d4a9ce5192a83e4fe959d78c4147b3760c Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Tue, 17 Mar 2026 22:17:50 -0500 Subject: [PATCH 012/313] chore: Scheduled updates (Firmware, Hardware, Translations, Graphs) (#4833) --- app/src/main/assets/firmware_releases.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/assets/firmware_releases.json b/app/src/main/assets/firmware_releases.json index 16680c478..15f158322 100644 --- a/app/src/main/assets/firmware_releases.json +++ b/app/src/main/assets/firmware_releases.json @@ -188,6 +188,12 @@ ] }, "pullRequests": [ + { + "id": "9934", + "title": "fix: MQTT settings silently fail to persist when broker is unreachable", + "page_url": "https://github.com/meshtastic/firmware/pull/9934", + "zip_url": "https://discord.com/invite/meshtastic" + }, { "id": "9931", "title": "fix: apply LoRa config changes live without rebooting", From 06c990026f4f0fa5f61c16c9fbec7484a42fd174 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 22:18:02 -0500 Subject: [PATCH 013/313] chore(deps): update google maps compose to v8.2.2 (#4834) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 388620382..11484d14c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,7 +35,7 @@ compose-multiplatform = "1.11.0-alpha04" jetbrains-adaptive = "1.3.0-alpha06" # Google -maps-compose = "8.2.1" +maps-compose = "8.2.2" # ML Kit mlkit-barcode-scanning = "17.3.0" From 59408ef46ec00c97cdf416a177226405c445e749 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Wed, 18 Mar 2026 07:42:24 -0500 Subject: [PATCH 014/313] feat: Desktop USB serial transport (#4836) Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- AGENTS.md | 8 +- GEMINI.md | 8 +- .../index.md | 5 + .../metadata.json | 8 + .../desktop_serial_transport_20260317/plan.md | 21 +++ .../desktop_serial_transport_20260317/spec.md | 20 +++ conductor/tech-stack.md | 10 +- core/network/build.gradle.kts | 8 +- .../core/network/SerialTransport.kt | 158 ++++++++++++++++++ .../core/network/SerialTransportTest.kt | 56 +++++++ desktop/README.md | 5 +- .../radio/DesktopRadioInterfaceService.kt | 25 ++- docs/kmp-status.md | 9 +- docs/roadmap.md | 35 ++-- .../CommonGetDiscoveredDevicesUseCase.kt | 18 +- .../connections/domain/usecase/UsbScanner.kt | 25 +++ .../domain/usecase/JvmUsbScanner.kt | 53 ++++++ .../connections/model/JvmUsbDeviceData.kt | 20 +++ gradle/libs.versions.toml | 2 + 19 files changed, 457 insertions(+), 37 deletions(-) create mode 100644 conductor/archive/desktop_serial_transport_20260317/index.md create mode 100644 conductor/archive/desktop_serial_transport_20260317/metadata.json create mode 100644 conductor/archive/desktop_serial_transport_20260317/plan.md create mode 100644 conductor/archive/desktop_serial_transport_20260317/spec.md create mode 100644 core/network/src/jvmMain/kotlin/org/meshtastic/core/network/SerialTransport.kt create mode 100644 core/network/src/jvmTest/kotlin/org/meshtastic/core/network/SerialTransportTest.kt create mode 100644 feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/domain/usecase/UsbScanner.kt create mode 100644 feature/connections/src/jvmMain/kotlin/org/meshtastic/feature/connections/domain/usecase/JvmUsbScanner.kt create mode 100644 feature/connections/src/jvmMain/kotlin/org/meshtastic/feature/connections/model/JvmUsbDeviceData.kt diff --git a/AGENTS.md b/AGENTS.md index b35b8d208..def726573 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,7 +38,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec | `core:repository` | High-level domain interfaces (e.g., `NodeRepository`, `LocationRepository`). | | `core:domain` | Pure KMP business logic and UseCases. | | `core:data` | Core manager implementations and data orchestration. | -| `core:network` | KMP networking layer using Ktor, MQTT abstractions, and shared transport (`StreamFrameCodec` in commonMain, `TcpTransport` in jvmAndroidMain). | +| `core:network` | KMP networking layer using Ktor, MQTT abstractions, and shared transport (`StreamFrameCodec`, `TcpTransport`, `SerialTransport`). | | `core:di` | Common DI qualifiers and dispatchers. | | `core:navigation` | Shared navigation keys/routes for Navigation 3. | | `core:ui` | Shared Compose UI components (`EmptyDetailPlaceholder`, `MainAppBar`, dialogs, preferences) and platform abstractions. | @@ -47,11 +47,11 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec | `core:prefs` | KMP preferences layer built on DataStore abstractions. | | `core:barcode` | Barcode scanning (Android-only). | | `core:nfc` | NFC abstractions (KMP). Android NFC hardware implementation in `androidMain`. | -| `core/ble/` | Bluetooth Low Energy stack using Nordic libraries. | +| `core/ble/` | Bluetooth Low Energy stack using Kable. | | `core/resources/` | Centralized string and image resources (Compose Multiplatform). | | `core/testing/` | **Shared test doubles, fakes, and utilities for `commonTest` across all KMP modules.** | | `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`, `node`, `intro`, `connections`). All are KMP with `jvm()` target. Use `meshtastic.kmp.feature` convention plugin. | -| `desktop/` | Compose Desktop application — first non-Android KMP target. Nav 3 shell, full Koin DI graph, TCP transport with `want_config` handshake. | +| `desktop/` | Compose Desktop application — first non-Android KMP target. Nav 3 shell, full Koin DI graph, TCP, Serial/USB, and BLE transports with `want_config` handshake. | | `mesh_service_example/` | Sample app showing `core:api` service integration. | ## 3. Development Guidelines & Coding Standards @@ -72,7 +72,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec - **Concurrency:** Use Kotlin Coroutines and Flow. - **Dependency Injection:** Use **Koin Annotations** with the K2 compiler plugin (0.4.0+). Keep root graph assembly in `app`. - **ViewModels:** Follow the MVI/UDF pattern. Use the multiplatform `androidx.lifecycle.ViewModel` in `commonMain`. -- **BLE:** All Bluetooth communication must route through `core:ble` using Nordic Semiconductor's Android Common Libraries. +- **BLE:** All Bluetooth communication must route through `core:ble` using Kable. - **Dependencies:** Check `gradle/libs.versions.toml` before assuming a library is available. - **JetBrains fork aliases:** Version catalog aliases for JetBrains-forked AndroidX artifacts use the `jetbrains-*` prefix (e.g., `jetbrains-lifecycle-runtime-compose`, `jetbrains-navigation3-ui`). Plain `androidx-*` aliases are true Google AndroidX artifacts. Never mix them up in `commonMain`. - **Compose Multiplatform:** Version catalog aliases for Compose Multiplatform artifacts use the `compose-multiplatform-*` prefix (e.g., `compose-multiplatform-material3`, `compose-multiplatform-foundation`). Never use plain `androidx.compose` dependencies in common Main. diff --git a/GEMINI.md b/GEMINI.md index b35b8d208..def726573 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -38,7 +38,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec | `core:repository` | High-level domain interfaces (e.g., `NodeRepository`, `LocationRepository`). | | `core:domain` | Pure KMP business logic and UseCases. | | `core:data` | Core manager implementations and data orchestration. | -| `core:network` | KMP networking layer using Ktor, MQTT abstractions, and shared transport (`StreamFrameCodec` in commonMain, `TcpTransport` in jvmAndroidMain). | +| `core:network` | KMP networking layer using Ktor, MQTT abstractions, and shared transport (`StreamFrameCodec`, `TcpTransport`, `SerialTransport`). | | `core:di` | Common DI qualifiers and dispatchers. | | `core:navigation` | Shared navigation keys/routes for Navigation 3. | | `core:ui` | Shared Compose UI components (`EmptyDetailPlaceholder`, `MainAppBar`, dialogs, preferences) and platform abstractions. | @@ -47,11 +47,11 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec | `core:prefs` | KMP preferences layer built on DataStore abstractions. | | `core:barcode` | Barcode scanning (Android-only). | | `core:nfc` | NFC abstractions (KMP). Android NFC hardware implementation in `androidMain`. | -| `core/ble/` | Bluetooth Low Energy stack using Nordic libraries. | +| `core/ble/` | Bluetooth Low Energy stack using Kable. | | `core/resources/` | Centralized string and image resources (Compose Multiplatform). | | `core/testing/` | **Shared test doubles, fakes, and utilities for `commonTest` across all KMP modules.** | | `feature/` | Feature modules (e.g., `settings`, `map`, `messaging`, `node`, `intro`, `connections`). All are KMP with `jvm()` target. Use `meshtastic.kmp.feature` convention plugin. | -| `desktop/` | Compose Desktop application — first non-Android KMP target. Nav 3 shell, full Koin DI graph, TCP transport with `want_config` handshake. | +| `desktop/` | Compose Desktop application — first non-Android KMP target. Nav 3 shell, full Koin DI graph, TCP, Serial/USB, and BLE transports with `want_config` handshake. | | `mesh_service_example/` | Sample app showing `core:api` service integration. | ## 3. Development Guidelines & Coding Standards @@ -72,7 +72,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec - **Concurrency:** Use Kotlin Coroutines and Flow. - **Dependency Injection:** Use **Koin Annotations** with the K2 compiler plugin (0.4.0+). Keep root graph assembly in `app`. - **ViewModels:** Follow the MVI/UDF pattern. Use the multiplatform `androidx.lifecycle.ViewModel` in `commonMain`. -- **BLE:** All Bluetooth communication must route through `core:ble` using Nordic Semiconductor's Android Common Libraries. +- **BLE:** All Bluetooth communication must route through `core:ble` using Kable. - **Dependencies:** Check `gradle/libs.versions.toml` before assuming a library is available. - **JetBrains fork aliases:** Version catalog aliases for JetBrains-forked AndroidX artifacts use the `jetbrains-*` prefix (e.g., `jetbrains-lifecycle-runtime-compose`, `jetbrains-navigation3-ui`). Plain `androidx-*` aliases are true Google AndroidX artifacts. Never mix them up in `commonMain`. - **Compose Multiplatform:** Version catalog aliases for Compose Multiplatform artifacts use the `compose-multiplatform-*` prefix (e.g., `compose-multiplatform-material3`, `compose-multiplatform-foundation`). Never use plain `androidx.compose` dependencies in common Main. diff --git a/conductor/archive/desktop_serial_transport_20260317/index.md b/conductor/archive/desktop_serial_transport_20260317/index.md new file mode 100644 index 000000000..1cbe07406 --- /dev/null +++ b/conductor/archive/desktop_serial_transport_20260317/index.md @@ -0,0 +1,5 @@ +# Track desktop_serial_transport_20260317 Context + +- [Specification](./spec.md) +- [Implementation Plan](./plan.md) +- [Metadata](./metadata.json) \ No newline at end of file diff --git a/conductor/archive/desktop_serial_transport_20260317/metadata.json b/conductor/archive/desktop_serial_transport_20260317/metadata.json new file mode 100644 index 000000000..3d1257289 --- /dev/null +++ b/conductor/archive/desktop_serial_transport_20260317/metadata.json @@ -0,0 +1,8 @@ +{ + "track_id": "desktop_serial_transport_20260317", + "type": "feature", + "status": "new", + "created_at": "2026-03-17T12:00:00Z", + "updated_at": "2026-03-17T12:00:00Z", + "description": "Implement Serial/USB transport for the Desktop target using jSerialComm. This fulfills the medium-term priority for direct radio connections on JVM and uses the shared RadioTransport interface." +} \ No newline at end of file diff --git a/conductor/archive/desktop_serial_transport_20260317/plan.md b/conductor/archive/desktop_serial_transport_20260317/plan.md new file mode 100644 index 000000000..3d55c7380 --- /dev/null +++ b/conductor/archive/desktop_serial_transport_20260317/plan.md @@ -0,0 +1,21 @@ +# Implementation Plan: Desktop Serial/USB Transport + +## Phase 1: JVM Setup & Dependency Integration [checkpoint: a05916d] +- [x] Task: Add the `jSerialComm` library to the `jvmMain` dependencies of the networking module. [checkpoint: 8994c66] +- [x] Task: Create a `jvmMain` stub implementation for a `SerialTransport` class that implements the shared `RadioTransport` interface. [checkpoint: 83668e4] + +## Phase 2: Serial Port Scanning & Connection Management [checkpoint: 9cda87d] +- [x] Task: Implement port discovery using `jSerialComm` to list available serial ports. [checkpoint: c72501d] +- [x] Task: Implement connect/disconnect logic for a selected serial port, handling port locking and baud rate configuration. [checkpoint: 23ee815] +- [x] Task: Map the input/output streams of the open serial port to the existing KMP stream framing logic (`StreamFrameCodec`). [checkpoint: 04ba9c2] + +## Phase 3: UI Integration +- [x] Task: Update the `feature:connections` UI or `DesktopScannerViewModel` to poll the new `SerialTransport` for available ports. [checkpoint: 2e85b5a] +- [x] Task: Wire the user's serial port selection to initiate the connection via the DI graph and active service logic. [checkpoint: 94cb97c] + +## Phase 4: Validation [checkpoint: 1055752] +- [x] Task: Verify end-to-end communication with a physical Meshtastic device over USB on the desktop target. [checkpoint: 1055752] +- [x] Task: Ensure CI builds cleanly and that no `java.*` dependencies leaked into `commonMain`. [checkpoint: 1055752] + +## Phase: Review Fixes +- [x] Task: Apply review suggestions [checkpoint: d2f7c82] diff --git a/conductor/archive/desktop_serial_transport_20260317/spec.md b/conductor/archive/desktop_serial_transport_20260317/spec.md new file mode 100644 index 000000000..04ff68481 --- /dev/null +++ b/conductor/archive/desktop_serial_transport_20260317/spec.md @@ -0,0 +1,20 @@ +# Specification: Desktop Serial/USB Transport via jSerialComm + +## Objective +Implement direct radio connection via Serial/USB on the Desktop (JVM) target using the `jSerialComm` library. This fulfills the medium-term priority of bringing physical transport parity to the desktop app and validates the newly extracted `RadioTransport` abstraction in `core:repository`. + +## Background +Currently, the desktop app supports TCP connections via a shared `StreamFrameCodec`. To provide parity with Android's USB serial connection capabilities, we need to implement a JVM-specific serial transport. The `jSerialComm` library is a widely-used, cross-platform Java library that handles native serial port communication without requiring complex JNI setups. + +## Requirements +- Introduce `jSerialComm` dependency to the `jvmMain` source set of the appropriate core module (likely `core:network` or a new `core:serial` module). +- Implement the `RadioTransport` interface (defined in `core:repository/commonMain`) for the desktop target, wrapping `jSerialComm`'s port scanning and connection logic. +- Ensure the serial data is encoded/decoded using the same protobuf frame structure utilized by the TCP transport (e.g., leveraging the existing `StreamFrameCodec`). +- Integrate the new transport into the `feature:connections` UI on the desktop so users can scan for and select connected USB serial devices. +- Retain platform purity: keep all `jSerialComm` and `java.io.*` imports strictly within the `jvmMain` source set. + +## Success Criteria +- [ ] Desktop application successfully scans for connected Meshtastic devices over USB/Serial. +- [ ] Users can select a serial port from the `feature:connections` UI and establish a connection. +- [ ] Two-way protobuf communication is verified (e.g., the app receives node info and can send a message). +- [ ] The implementation uses the shared `RadioTransport` interface without leaking JVM dependencies into `commonMain`. diff --git a/conductor/tech-stack.md b/conductor/tech-stack.md index c6ea7ebbd..eb3244a32 100644 --- a/conductor/tech-stack.md +++ b/conductor/tech-stack.md @@ -24,4 +24,12 @@ ## Networking & Transport - **Ktor:** Multiplatform HTTP client for web services and TCP streaming. - **Kable:** Multiplatform BLE library used as the primary BLE transport for all targets (Android, Desktop, and future iOS). -- **Coroutines & Flows:** For asynchronous programming and state management. \ No newline at end of file +- **jSerialComm:** Cross-platform Java library used for direct Serial/USB communication with Meshtastic devices on the Desktop (JVM) target. +- **Coroutines & Flows:** For asynchronous programming and state management. + +## Testing (KMP) +- **Shared Tests First:** The majority of business logic, ViewModels, and state interactions are tested in the `commonTest` source set using standard `kotlin.test`. +- **Coroutines Testing:** Use `kotlinx-coroutines-test` for virtual time management in asynchronous flows. +- **Mocking Strategy:** Avoid JVM-specific mocking libraries. Prefer `Mokkery` or `Mockative` for multiplatform-compatible mocking interfaces, alongside handwritten fakes in `core:testing`. +- **Flow Assertions:** Use `Turbine` for testing multiplatform `Flow` emissions and state updates. +- **Property-Based Testing:** Consider evaluating `Kotest` for multiplatform data-driven and property-based testing scenarios if standard `kotlin.test` becomes insufficient. \ No newline at end of file diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index dde171d11..a499f3644 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -48,7 +48,12 @@ kotlin { implementation(libs.kermit) } - val jvmMain by getting { dependencies { implementation(libs.ktor.client.java) } } + val jvmMain by getting { + dependencies { + implementation(libs.ktor.client.java) + implementation(libs.jserialcomm) + } + } androidMain.dependencies { implementation(projects.core.ble) @@ -61,6 +66,7 @@ kotlin { implementation(libs.okhttp3.logging.interceptor) } + val jvmTest by getting { dependencies { implementation(libs.mockk) } } commonTest.dependencies { implementation(libs.kotlinx.coroutines.test) } } } 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 new file mode 100644 index 000000000..7e504f893 --- /dev/null +++ b/core/network/src/jvmMain/kotlin/org/meshtastic/core/network/SerialTransport.kt @@ -0,0 +1,158 @@ +/* + * 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.network + +import co.touchlab.kermit.Logger +import com.fazecast.jSerialComm.SerialPort +import com.fazecast.jSerialComm.SerialPortTimeoutException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import org.meshtastic.core.network.radio.StreamInterface +import org.meshtastic.core.repository.RadioInterfaceService + +/** + * JVM-specific implementation of [RadioTransport] using jSerialComm. Uses [StreamInterface] for START1/START2 packet + * framing. + */ +class SerialTransport( + private val portName: String, + private val baudRate: Int = DEFAULT_BAUD_RATE, + service: RadioInterfaceService, +) : StreamInterface(service) { + private var serialPort: SerialPort? = null + private var readJob: Job? = null + + /** Attempts to open the serial port and starts the read loop. Returns true if successful, false otherwise. */ + fun startConnection(): Boolean { + return try { + val port = SerialPort.getCommPort(portName) ?: return false + port.setComPortParameters(baudRate, DATA_BITS, SerialPort.ONE_STOP_BIT, SerialPort.NO_PARITY) + port.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING, READ_TIMEOUT_MS, 0) + if (port.openPort()) { + serialPort = port + port.setDTR() + port.setRTS() + super.connect() // Sends WAKE_BYTES and signals service.onConnect() + startReadLoop(port) + true + } else { + false + } + } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { + Logger.e(e) { "Serial connection failed" } + false + } + } + + @Suppress("CyclomaticComplexMethod") + private fun startReadLoop(port: SerialPort) { + readJob = + service.serviceScope.launch(Dispatchers.IO) { + val input = port.inputStream + val buffer = ByteArray(READ_BUFFER_SIZE) + try { + var reading = true + while (isActive && port.isOpen && reading) { + try { + val numRead = input.read(buffer) + if (numRead == -1) { + reading = false + } else if (numRead > 0) { + for (i in 0 until numRead) { + readChar(buffer[i]) + } + } + } catch (_: SerialPortTimeoutException) { + // Expected timeout when no data is available + } catch (e: kotlinx.coroutines.CancellationException) { + throw e + } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { + if (isActive) { + Logger.e(e) { "Serial read IOException: ${e.message}" } + } else { + Logger.d { "Serial read interrupted by cancellation: ${e.message}" } + } + reading = false + } + } + } catch (e: kotlinx.coroutines.CancellationException) { + throw e + } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { + if (isActive) { + Logger.e(e) { "Serial read loop outer error: ${e.message}" } + } else { + Logger.d { "Serial read loop outer interrupted by cancellation: ${e.message}" } + } + } finally { + try { + input.close() + } catch (_: Exception) { + // Ignore errors during input stream close + } + try { + if (port.isOpen) { + port.closePort() + } + } catch (_: Exception) { + // Ignore errors during port close + } + if (isActive) { + onDeviceDisconnect(true) + } + } + } + } + + override fun sendBytes(p: ByteArray) { + serialPort?.takeIf { it.isOpen }?.outputStream?.write(p) + } + + override fun flushBytes() { + serialPort?.takeIf { it.isOpen }?.outputStream?.flush() + } + + override fun keepAlive() { + // Not specifically needed for raw serial unless implemented + } + + private fun closePortResources() { + serialPort?.takeIf { it.isOpen }?.closePort() + serialPort = null + } + + override fun close() { + readJob?.cancel() + readJob = null + closePortResources() + super.close() + } + + companion object { + private const val DEFAULT_BAUD_RATE = 115200 + private const val DATA_BITS = 8 + private const val READ_BUFFER_SIZE = 1024 + private const val READ_TIMEOUT_MS = 100 + + /** + * Discovers and returns a list of available serial ports. Returns a list of the system port names (e.g., + * "COM3", "/dev/ttyUSB0"). + */ + fun getAvailablePorts(): List = SerialPort.getCommPorts().map { it.systemPortName } + } +} diff --git a/core/network/src/jvmTest/kotlin/org/meshtastic/core/network/SerialTransportTest.kt b/core/network/src/jvmTest/kotlin/org/meshtastic/core/network/SerialTransportTest.kt new file mode 100644 index 000000000..ab1e408ae --- /dev/null +++ b/core/network/src/jvmTest/kotlin/org/meshtastic/core/network/SerialTransportTest.kt @@ -0,0 +1,56 @@ +/* + * 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.network + +import com.fazecast.jSerialComm.SerialPort +import io.mockk.mockk +import org.meshtastic.core.repository.RadioInterfaceService +import org.meshtastic.core.repository.RadioTransport +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class SerialTransportTest { + private val mockService: RadioInterfaceService = mockk(relaxed = true) + + @Test + fun testJSerialCommIsAvailable() { + val ports = SerialPort.getCommPorts() + assertNotNull(ports, "Serial ports array should not be null") + } + + @Test + fun testSerialTransportImplementsRadioTransport() { + val transport: RadioTransport = SerialTransport("dummyPort", service = mockService) + assertTrue(transport is SerialTransport, "Transport should be a SerialTransport") + } + + @Test + fun testGetAvailablePorts() { + val ports = SerialTransport.getAvailablePorts() + assertNotNull(ports, "Available ports should not be null") + } + + @Test + fun testConnectToInvalidPortFailsGracefully() { + val transport = SerialTransport("invalid_port_name", 115200, mockService) + val connected = transport.startConnection() + assertFalse(connected, "Connecting to an invalid port should return false") + transport.close() + } +} diff --git a/desktop/README.md b/desktop/README.md index 51485da04..14a66457f 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -49,7 +49,7 @@ The module depends on the JVM variants of KMP modules: | `navigation/DesktopSettingsNavigation.kt` | Real settings feature composables wired into nav graph (~35 screens) | | `navigation/DesktopNodeNavigation.kt` | Real adaptive node list-detail + real metrics screens (logs + charts); map routes remain placeholders | | `navigation/DesktopMessagingNavigation.kt` | Real adaptive contacts list-detail + real Messages/Share/QuickChat route screens | -| `radio/DesktopRadioInterfaceService.kt` | TCP socket transport with auto-reconnect, heartbeat, and backoff retry | +| `radio/DesktopRadioInterfaceService.kt` | TCP, Serial/USB, and BLE transports with auto-reconnect, heartbeat, and backoff retry | | `radio/DesktopMeshServiceController.kt` | Mesh service lifecycle — orchestrates `want_config` handshake chain | | `radio/DesktopMessageQueue.kt` | Message queue for outbound mesh packets | | `ui/firmware/DesktopFirmwareScreen.kt` | Placeholder firmware screen (native DFU is Android-only) | @@ -91,6 +91,7 @@ The module depends on the JVM variants of KMP modules: - [x] Add desktop language picker backed by shared `UiPreferencesDataSource.locale` with live translation updates - [ ] Wire remaining `feature:*` composables (map) into the nav graph - [ ] Move remaining node detail and message composables from `androidMain` to `commonMain` -- [ ] Add serial/USB transport for direct radio connection on Desktop +- [x] Add serial/USB transport for direct radio connection on Desktop +- [x] Add BLE transport (via Kable) for direct radio connection on Desktop - [ ] Add MQTT transport for cloud-connected operation - [x] Package as native distributions (DMG, MSI, DEB) via CI release pipeline diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/radio/DesktopRadioInterfaceService.kt b/desktop/src/main/kotlin/org/meshtastic/desktop/radio/DesktopRadioInterfaceService.kt index 22d47e012..c4defd7d1 100644 --- a/desktop/src/main/kotlin/org/meshtastic/desktop/radio/DesktopRadioInterfaceService.kt +++ b/desktop/src/main/kotlin/org/meshtastic/desktop/radio/DesktopRadioInterfaceService.kt @@ -56,7 +56,11 @@ class DesktopRadioInterfaceService( ) : RadioInterfaceService { override val supportedDeviceTypes: List = - listOf(org.meshtastic.core.model.DeviceType.TCP, org.meshtastic.core.model.DeviceType.BLE) + listOf( + org.meshtastic.core.model.DeviceType.TCP, + org.meshtastic.core.model.DeviceType.BLE, + org.meshtastic.core.model.DeviceType.USB, + ) private val _connectionState = MutableStateFlow(ConnectionState.Disconnected) override val connectionState: StateFlow = _connectionState.asStateFlow() @@ -76,6 +80,7 @@ class DesktopRadioInterfaceService( private var transport: TcpTransport? = null private var bleTransport: DesktopBleInterface? = null + private var serialTransport: org.meshtastic.core.network.SerialTransport? = null init { // Observe radioPrefs to handle asynchronous loads from DataStore @@ -136,6 +141,7 @@ class DesktopRadioInterfaceService( serviceScope.handledLaunch { transport?.sendPacket(bytes) bleTransport?.handleSendToRadio(bytes) + serialTransport?.handleSendToRadio(bytes) } } @@ -170,6 +176,8 @@ class DesktopRadioInterfaceService( private fun startConnection(address: String) { if (address.startsWith("t")) { startTcpConnection(address.removePrefix("t")) + } else if (address.startsWith("s")) { + startSerialConnection(address.removePrefix("s")) } else if (address.startsWith("x")) { startBleConnection(address.removePrefix("x")) } else { @@ -179,6 +187,18 @@ class DesktopRadioInterfaceService( } } + private fun startSerialConnection(portName: String) { + transport?.stop() + bleTransport?.close() + serialTransport?.close() + + val serial = org.meshtastic.core.network.SerialTransport(portName = portName, service = this) + serialTransport = serial + if (!serial.startConnection()) { + onDisconnect(isPermanent = true, errorMessage = "Failed to connect to $portName") + } + } + private fun startBleConnection(address: String) { transport?.stop() bleTransport?.close() @@ -228,6 +248,9 @@ class DesktopRadioInterfaceService( bleTransport?.close() bleTransport = null + serialTransport?.close() + serialTransport = null + // Recreate the service scope serviceScope.cancel("stopping interface") serviceScope = CoroutineScope(dispatchers.io + SupervisorJob()) diff --git a/docs/kmp-status.md b/docs/kmp-status.md index 2f5f2861f..4e9811a3e 100644 --- a/docs/kmp-status.md +++ b/docs/kmp-status.md @@ -27,7 +27,7 @@ Modules that share JVM-specific code between Android and desktop now standardize | `core:database` | ✅ | ✅ | Room KMP | | `core:domain` | ✅ | ✅ | UseCases | | `core:prefs` | ✅ | ✅ | Preferences layer | -| `core:network` | ✅ | ✅ | Ktor, `StreamFrameCodec`, `TcpTransport` | +| `core:network` | ✅ | ✅ | Ktor, `StreamFrameCodec`, `TcpTransport`, `SerialTransport` | | `core:data` | ✅ | ✅ | Data orchestration | | `core:ble` | ✅ | ✅ | Kable multiplatform BLE abstractions in commonMain | | `core:nfc` | ✅ | ✅ | NFC contract in commonMain; hardware in androidMain | @@ -56,13 +56,14 @@ Modules that share JVM-specific code between Android and desktop now standardize Working Compose Desktop application with: - Navigation 3 shell (`NavigationRail` + `NavDisplay`) using shared routes - Full Koin DI graph (stubs + real implementations) -- TCP transport with auto-reconnect and full `want_config` handshake +- TCP, Serial/USB, and BLE transports with auto-reconnect and full `want_config` handshake - Adaptive list-detail screens for nodes and contacts -- **Dynamic Connections screen** with automatic discovery of platform-supported transports (TCP) +- **Dynamic Connections screen** with automatic discovery of platform-supported transports (TCP, Serial/USB, BLE) - **Desktop language picker** backed by `UiPreferencesDataSource.locale`, with immediate Compose Multiplatform resource updates - **Navigation-preserving locale switching** via `Main.kt` `staticCompositionLocalOf` recomposition instead of recreating the Nav3 backstack - Node detail metrics screens (Device, Environment, Signal, Power, Pax) wired with shared KMP + Vico charts - 7 desktop-specific screens (Settings, Device, Position, Network, Security, ExternalNotification, Debug) +- **Native notifications and system tray icon** wired via `DesktopNotificationManager` - **Native release pipeline** generating `.dmg` (macOS), `.msi` (Windows), and `.deb` (Linux) installers in CI ## Scorecard @@ -107,7 +108,7 @@ Based on the latest codebase investigation, the following steps are proposed to | Material 3 Adaptive (JetBrains) | ✅ Done | Version `1.3.0-alpha06` aligned with CMP `1.11.0-alpha04` | | JetBrains lifecycle/nav3 alias alignment | ✅ Done | All forked deps use `jetbrains-*` prefix in version catalog; `core:data` commonMain uses JetBrains lifecycle runtime | | Expect/actual consolidation | ✅ Done | 7 pairs eliminated; 15+ genuinely platform-specific retained | -| Transport deduplication | ✅ Done | `StreamFrameCodec` + `TcpTransport` shared in `core:network` | +| Transport deduplication | ✅ Done | `StreamFrameCodec`, `TcpTransport`, and `SerialTransport` shared in `core:network` | | **Transport UI Unification** | ✅ Done | `RadioInterfaceService` provides dynamic transport capability to shared UI | | Emoji picker unification | ✅ Done | Single commonMain implementation replacing 3 platform variants | diff --git a/docs/roadmap.md b/docs/roadmap.md index 01fb9402e..0dd6adc5e 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -28,7 +28,7 @@ here| **Migrate to JetBrains Compose Multiplatform dependencies** | High | Low | - ✅ **Settings:** ~35 screens with real configuration, including theme/about parity and desktop language picker support - ✅ **Nodes:** Adaptive list-detail with node management - ✅ **Messaging:** Adaptive contacts with message view + send -- ✅ **Connections:** Dynamic discovery of platform-supported transports (TCP) +- ✅ **Connections:** Dynamic discovery of platform-supported transports (TCP, Serial/USB, BLE) - ❌ **Map:** Placeholder only, needs MapLibre or alternative - ⚠️ **Firmware:** Placeholder wired into nav graph; native DFU not applicable to desktop - ⚠️ **Intro:** Onboarding flow (may not apply to desktop) @@ -41,7 +41,7 @@ here| **Migrate to JetBrains Compose Multiplatform dependencies** | High | Low | - Test navigation flows end-to-end 2. **Tier 2: Polish (High Priority)** - Additional desktop-specific settings polish - - Keyboard shortcuts + - ✅ **MenuBar integration** and Keyboard shortcuts - Window management - State persistence 3. **Tier 3: Advanced (Nice-to-have)** @@ -53,9 +53,10 @@ here| **Migrate to JetBrains Compose Multiplatform dependencies** | High | Low | | Transport | Platform | Status | |---|---|---| | TCP | Desktop (JVM) | ✅ Done — shared `StreamFrameCodec` + `TcpTransport` in `core:network` | -| Serial/USB | Desktop (JVM) | ❌ Next — jSerialComm | +| Serial/USB | Desktop (JVM) | ✅ Done — jSerialComm | | MQTT | All (KMP) | ❌ Planned — Ktor/MQTT (currently Android-only via Eclipse Paho) | -| BLE | Desktop | ❌ Future — Kable (JVM) | +| BLE | Android | ✅ Done — Kable | +| BLE | Desktop | ✅ Done — Kable (JVM) | | BLE | iOS | ❌ Future — Kable/CoreBluetooth | ### Desktop Feature Gaps @@ -70,6 +71,8 @@ here| **Migrate to JetBrains Compose Multiplatform dependencies** | High | Low | | Map | ❌ Needs MapLibre or equivalent | | Charts | ✅ Vico KMP charts wired in commonMain (Device, Environment, Signal, Power, Pax) | | Debug Panel | ✅ Real screen (mesh log viewer via shared `DebugViewModel`) | +| Notifications | ✅ Desktop native notifications with system tray icon support | +| MenuBar | ✅ Done — Native application menu bar with File/View menus | | About | ✅ Shared `commonMain` screen (AboutLibraries KMP `produceLibraries` + per-platform JSON) | | Packaging | ✅ Done — Native distribution pipeline in CI (DMG, MSI, DEB) | @@ -89,9 +92,9 @@ here| **Migrate to JetBrains Compose Multiplatform dependencies** | High | Low | - ✅ **Done:** Extracted remaining 5 ViewModels: `SettingsViewModel`, `RadioConfigViewModel`, `DebugViewModel`, `MetricsViewModel`, `UIViewModel` to shared KMP modules. - ✅ **Done:** Extracted service, worker, and radio files from `app` to `core:service/androidMain` and `core:network/androidMain`. - **Next:** Extract remaining Android-specific files (e.g., Navigation files, App Widgets, message queues, and root Activity logic) out of `:app` to establish a truly thin app module. -2. **Serial/USB transport** — direct radio connection on Desktop via jSerialComm +2. ✅ **Done:** **Serial/USB transport** — direct radio connection on Desktop via jSerialComm 3. **MQTT transport** — cloud relay operation (KMP, benefits all targets) -4. **Evaluate KMP-native mocking** — Evaluate `mockative` or similar to replace `mockk` in `commonMain` of `core:testing` for iOS readiness. +4. **Evaluate KMP-native testing tools** — Evaluate `Mokkery` or `Mockative` to replace `mockk` in `commonMain` of `core:testing` for iOS readiness. Integrate `Turbine` for shared `Flow` testing. 5. **Desktop ViewModel auto-wiring** — ✅ Done: ensured Koin K2 Compiler Plugin generates ViewModel modules for JVM target; eliminated manual wiring in `DesktopKoinModule` 5. **KMP charting** — ✅ Done: Vico charts migrated to `feature:node/commonMain` using KMP artifacts; desktop wires them directly 6. **Navigation contract extraction** — ✅ Done: shared `TopLevelDestination` enum in `core:navigation`; icon mapping in `core:ui`; parity tests in place. Both shells derive from the same source of truth. @@ -100,17 +103,23 @@ here| **Migrate to JetBrains Compose Multiplatform dependencies** | High | Low | ## Longer-Term (90+ days) 1. **iOS proof target** — declare `iosArm64()`/`iosSimulatorArm64()` in KMP modules; BLE via Kable/CoreBluetooth -2. **Map on Desktop** — evaluate MapLibre for cross-platform maps +2. **Platform-Native UI Interop** — + - **iOS Maps & Camera:** Implement `MapLibre` or `MKMapView` via Compose Multiplatform's `UIKitView`. Leverage `AVCaptureSession` wrapped in `UIKitView` to fulfill the `LocalBarcodeScannerProvider` contract. + - **Desktop Maps:** Implement maps via `SwingPanel` wrapper, utilizing experimental interop blending (`compose.interop.blending=true`) to ensure tooltips and Compose overlays render correctly on top of the native JComponent. + - **Web (wasmJs) Integrations:** Leverage `HtmlView` to embed raw DOM elements (e.g., `