diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/TakServerIntegration.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/TakServerIntegration.kt
new file mode 100644
index 0000000000..0c2b811f60
--- /dev/null
+++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/TakServerIntegration.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.repository
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.StateFlow
+
+/**
+ * The minimal seam [org.meshtastic.core.service.MeshServiceOrchestrator] needs onto the TAK server integration —
+ * folding `TAKServerManager.isRunning` and `TAKMeshIntegration.start`/`stop` (core:takserver) into one interface so a
+ * platform with no TAK support (e.g. wasmJs, where core:takserver's TLS listener is a permanent browser-sandbox
+ * impossibility) can supply a real no-op without core:service depending on core:takserver at all.
+ */
+interface TakServerIntegration {
+ /** Whether the TAK server + mesh bridge are currently running. */
+ val isRunning: StateFlow
+
+ /** Start the TAK server and the mesh<->CoT bridge on [scope]. */
+ fun start(scope: CoroutineScope)
+
+ /** Stop the TAK server and the mesh<->CoT bridge. */
+ fun stop()
+}
diff --git a/core/service/build.gradle.kts b/core/service/build.gradle.kts
index 50dbe35556..5bbc01fa54 100644
--- a/core/service/build.gradle.kts
+++ b/core/service/build.gradle.kts
@@ -23,6 +23,13 @@ plugins {
kotlin {
android { withHostTest { isIncludeAndroidResources = true } }
+ // Library module: bare wasmJs(), no browser() (that's for the eventual webApp executable). No custom
+ // hierarchy group is needed for MAIN: core:takserver (the one dependency with no wasmJs variant) is
+ // removed entirely below, not relocated, and nothing else in commonMain/androidMain/jvmMain is
+ // web-hostile — same shape as core:repository, unlike core:ble/core:database/core:prefs/core:network.
+ @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
+ wasmJs()
+
sourceSets {
commonMain.dependencies {
api(projects.core.repository)
@@ -37,7 +44,9 @@ kotlin {
implementation(projects.core.prefs)
implementation(projects.core.resources)
implementation(libs.meshtastic.protobufs)
- implementation(projects.core.takserver)
+ // core:takserver removed (was only used by MeshServiceOrchestrator, which now depends on
+ // core:repository's TakServerIntegration seam instead — see TakServerIntegration.kt).
+ // Confirmed via grep: nothing else in this module references org.meshtastic.core.takserver.
implementation(libs.jetbrains.lifecycle.runtime)
implementation(libs.kotlinx.atomicfu)
@@ -60,6 +69,33 @@ kotlin {
}
}
- commonTest.dependencies { implementation(projects.core.testing) }
+ // TEST only: three of this module's six commonTest files don't compile for wasmJs, for two
+ // unrelated reasons, both confirmed empirically via a real compileTestKotlinWasmJs run (never
+ // guessed): (1) SharedRadioInterfaceServiceLivenessTest.kt depends on core:testing, which has no
+ // wasmJs target (same gap core:ble/core:database/core:network/core:repository each hit).
+ // (2) RadioControllerImplTest.kt and RadioControllerRestoreTest.kt both crash the Kotlin/Wasm
+ // compiler ("Serialization of IrErrorType is not supported anymore") -- bisected to exactly these
+ // two files (the other four compile fine); the one thing both do that no passing file does is
+ // construct a real RadioControllerImpl, which delegates to four collaborators via `by` (interface
+ // delegation) alongside Lazy/default-value constructor params. That combination is the leading
+ // suspect, not confirmed as the exact trigger -- this looks like a genuine Kotlin/Wasm backend
+ // limitation, not a missing library, and is out of this module's scope to fix. A plain additional
+ // source set -- not a full applyHierarchyTemplate reset -- keeps this scoped to test only, since
+ // MAIN needs no split at all (unlike core:ble/core:database/core:prefs/core:network).
+ //
+ // jvmTest/androidHostTest are leaf source sets tied 1:1 to their registered target, created
+ // synchronously and so already exist here — but the shared "iosTest" intermediate the default
+ // hierarchy template would otherwise provide is NOT materialized this early (its creation is
+ // deferred; confirmed empirically: `getByName("iosTest")` here throws "KotlinSourceSet with name
+ // 'iosTest' not found", while jvmTest/androidHostTest resolve fine). Wire the two iOS leaf test
+ // source sets directly instead.
+ val nonWebTest by creating {
+ dependsOn(commonTest.get())
+ dependencies { implementation(projects.core.testing) }
+ }
+ getByName("jvmTest") { dependsOn(nonWebTest) }
+ getByName("androidHostTest") { dependsOn(nonWebTest) }
+ matching { it.name == "iosArm64Test" || it.name == "iosSimulatorArm64Test" }
+ .configureEach { dependsOn(nonWebTest) }
}
}
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 5a6d225896..4c83865802 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
@@ -42,11 +42,10 @@ import org.meshtastic.core.repository.NodeManager
import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.core.repository.ServiceStateWriter
import org.meshtastic.core.repository.TakPrefs
+import org.meshtastic.core.repository.TakServerIntegration
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.getStringSuspend
import org.meshtastic.core.resources.local_network_permission_denied_hint
-import org.meshtastic.core.takserver.TAKMeshIntegration
-import org.meshtastic.core.takserver.TAKServerManager
// English fallback for local_network_permission_denied_hint when resource lookup is unavailable. Kept above the
// class KDoc so that KDoc still binds to the class. Internal so tests can assert the exact surfaced text.
@@ -70,8 +69,7 @@ class MeshServiceOrchestrator(
private val nodeManager: NodeManager,
private val messageProcessor: MeshMessageProcessor,
private val serviceNotifications: MeshNotificationManager,
- private val takServerManager: TAKServerManager,
- private val takMeshIntegration: TAKMeshIntegration,
+ private val takServerIntegration: TakServerIntegration,
private val takPrefs: TakPrefs,
private val databaseManager: DatabaseManager,
private val connectionManager: MeshConnectionManager,
@@ -167,12 +165,12 @@ class MeshServiceOrchestrator(
// Observe TAK server pref to start/stop
takPrefs.isTakServerEnabled
.onEach { isEnabled ->
- if (isEnabled && !takServerManager.isRunning.value) {
+ if (isEnabled && !takServerIntegration.isRunning.value) {
Logger.i { "TAK Server enabled by preference, starting integration" }
- takMeshIntegration.start(newScope)
+ takServerIntegration.start(newScope)
} else if (!isEnabled) {
Logger.i { "TAK Server disabled by preference, stopping integration" }
- takMeshIntegration.stop()
+ takServerIntegration.stop()
}
}
.launchIn(newScope)
@@ -223,7 +221,7 @@ class MeshServiceOrchestrator(
*/
fun stop() {
Logger.i { "Stopping mesh service orchestrator" }
- takMeshIntegration.stop()
+ takServerIntegration.stop()
// Best-effort polite goodbye on service teardown (onDestroy / process shutdown). We launch
// on a fresh detached scope — not the orchestrator's per-start scope — so the subsequent
// scope.cancel() below doesn't interrupt the short drain delay inside disconnect(). The
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
index 7b26c2a1e8..b91fe2cd5e 100644
--- a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/MeshServiceOrchestratorTest.kt
+++ b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/MeshServiceOrchestratorTest.kt
@@ -38,26 +38,20 @@ import org.meshtastic.core.common.database.DatabaseManager
import org.meshtastic.core.common.util.safeCatchingAll
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.model.ConnectionState
-import org.meshtastic.core.repository.CommandSender
-import org.meshtastic.core.repository.MeshConfigHandler
import org.meshtastic.core.repository.MeshConnectionManager
import org.meshtastic.core.repository.MeshMessageProcessor
import org.meshtastic.core.repository.MeshNotificationManager
import org.meshtastic.core.repository.NodeManager
-import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.core.repository.RadioSessionContext
import org.meshtastic.core.repository.ReceivedRadioFrame
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.repository.TakPrefs
+import org.meshtastic.core.repository.TakServerIntegration
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.getStringSuspend
import org.meshtastic.core.resources.local_network_permission_denied_hint
-import org.meshtastic.core.takserver.MeshToCotBroadcaster
-import org.meshtastic.core.takserver.TAKMeshIntegration
-import org.meshtastic.core.takserver.TAKServerManager
import org.meshtastic.proto.FromRadio
-import org.meshtastic.proto.LocalModuleConfig
import org.meshtastic.proto.MyNodeInfo
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -75,12 +69,9 @@ class MeshServiceOrchestratorTest {
private val nodeManager: NodeManager = mock(MockMode.autofill)
private val messageProcessor: MeshMessageProcessor = mock(MockMode.autofill)
- private val commandSender: CommandSender = mock(MockMode.autofill)
- private val meshConfigHandler: MeshConfigHandler = mock(MockMode.autofill)
private val serviceNotifications: MeshNotificationManager = mock(MockMode.autofill)
- private val takServerManager: TAKServerManager = mock(MockMode.autofill)
+ private val takServerIntegration: TakServerIntegration = mock(MockMode.autofill)
private val takPrefs: TakPrefs = mock(MockMode.autofill)
- private val nodeRepository: NodeRepository = mock(MockMode.autofill)
private val databaseManager: DatabaseManager = mock(MockMode.autofill)
private val connectionManager: MeshConnectionManager = mock(MockMode.autofill)
@@ -120,40 +111,8 @@ class MeshServiceOrchestratorTest {
{
isSessionActive(it.args[0] as RadioSessionContext)
}
- every { serviceRepository.meshPacketFlow } returns MutableSharedFlow()
- every { meshConfigHandler.moduleConfig } returns MutableStateFlow(LocalModuleConfig())
every { takPrefs.isTakServerEnabled } returns takEnabledFlow
- every { takPrefs.isMeshToCotEnabled } returns MutableStateFlow(false)
- every { takPrefs.takServerChannel } returns MutableStateFlow(0)
- every { takServerManager.isRunning } returns takRunningFlow
- every { takServerManager.inboundMessages } returns MutableSharedFlow()
- every { nodeRepository.myNodeInfo } returns MutableStateFlow(null)
-
- // Deliberately its own dispatcher, not the class-level testDispatcher: the broadcaster's
- // scheduler doesn't need to be the same one driving this test, and a distinct name keeps
- // that from reading as though the two are linked.
- val broadcasterTestDispatcher = UnconfinedTestDispatcher()
- val takMeshIntegration =
- TAKMeshIntegration(
- takServerManager = takServerManager,
- commandSender = commandSender,
- serviceRepository = serviceRepository,
- meshConfigHandler = meshConfigHandler,
- nodeRepository = nodeRepository,
- takPrefs = takPrefs,
- meshToCotBroadcaster =
- MeshToCotBroadcaster(
- takServerManager = takServerManager,
- nodeRepository = nodeRepository,
- takPrefs = takPrefs,
- dispatchers =
- CoroutineDispatchers(
- io = broadcasterTestDispatcher,
- main = broadcasterTestDispatcher,
- default = broadcasterTestDispatcher,
- ),
- ),
- )
+ every { takServerIntegration.isRunning } returns takRunningFlow
return MeshServiceOrchestrator(
radioInterfaceService = radioInterfaceService,
@@ -161,8 +120,7 @@ class MeshServiceOrchestratorTest {
nodeManager = nodeManager,
messageProcessor = messageProcessor,
serviceNotifications = serviceNotifications,
- takServerManager = takServerManager,
- takMeshIntegration = takMeshIntegration,
+ takServerIntegration = takServerIntegration,
takPrefs = takPrefs,
databaseManager = databaseManager,
connectionManager = connectionManager,
@@ -202,14 +160,23 @@ class MeshServiceOrchestratorTest {
// Toggle on
takEnabledFlow.value = true
- verify { takServerManager.start(any()) }
+ // Exactly 1, not just "at least 1": nothing before this point could have called start() — the
+ // orchestrator's collector's only prior emission (isTakServerEnabled's initial `false`) takes the
+ // stop() branch instead — so this genuinely proves the toggle drove it.
+ verify(exactly(1)) { takServerIntegration.start(any()) }
// Update mock state to reflect it's running
takRunningFlow.value = true
// Toggle off
takEnabledFlow.value = false
- verify { takServerManager.stop() }
+ // Exactly 2, not 1: the real TAKMeshIntegration this mock replaces has its own start()/stop()
+ // re-entrancy latch, so its initial no-op stop() (from isTakServerEnabled's starting `false`
+ // value, before "Toggle on" above) never reached the wrapped TAKServerManager it forwards to. A
+ // bare interface mock has no such latch — the orchestrator really does call stop() twice here
+ // (once at that initial emission, once for this toggle) — so asserting exactly(1) would fail,
+ // and a bare `verify { stop() }` would pass without proving *this* toggle caused a call.
+ verify(exactly(2)) { takServerIntegration.stop() }
orchestrator.stop()
}
@@ -219,14 +186,26 @@ class MeshServiceOrchestratorTest {
val takEnabledFlow = MutableStateFlow(false)
val takRunningFlow = MutableStateFlow(false)
val lifecycleEvents = mutableListOf()
- every { takServerManager.start(any()) } calls
+ // The real TAKMeshIntegration this mock replaces has its own start()/stop() re-entrancy latch
+ // (a CAS'd AtomicBoolean) so a redundant stop() while already stopped — or a redundant start()
+ // while already started — is a silent no-op instead of forwarding to TAKServerManager. A bare
+ // autofill mock has no such state, so replicate the latch here to keep asserting the real
+ // lifecycle sequence rather than every call the orchestrator happens to make.
+ var started = false
+ every { takServerIntegration.start(any()) } calls
{
- lifecycleEvents += "start"
+ if (!started) {
+ started = true
+ lifecycleEvents += "start"
+ }
Unit
}
- every { takServerManager.stop() } calls
+ every { takServerIntegration.stop() } calls
{
- lifecycleEvents += "stop"
+ if (started) {
+ started = false
+ lifecycleEvents += "stop"
+ }
Unit
}
val orchestrator = createOrchestrator(takEnabledFlow = takEnabledFlow, takRunningFlow = takRunningFlow)
@@ -248,14 +227,23 @@ class MeshServiceOrchestratorTest {
val takEnabledFlow = MutableStateFlow(true)
val takRunningFlow = MutableStateFlow(false)
val lifecycleEvents = mutableListOf()
- every { takServerManager.start(any()) } calls
+ // See testTakServerCanRetryAfterFailedStart for why this latch replicates the real
+ // TAKMeshIntegration's own start()/stop() re-entrancy guard.
+ var started = false
+ every { takServerIntegration.start(any()) } calls
{
- lifecycleEvents += "start"
+ if (!started) {
+ started = true
+ lifecycleEvents += "start"
+ }
Unit
}
- every { takServerManager.stop() } calls
+ every { takServerIntegration.stop() } calls
{
- lifecycleEvents += "stop"
+ if (started) {
+ started = false
+ lifecycleEvents += "stop"
+ }
Unit
}
val orchestrator = createOrchestrator(takEnabledFlow = takEnabledFlow, takRunningFlow = takRunningFlow)
diff --git a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/RadioControllerImplTest.kt b/core/service/src/nonWebTest/kotlin/org/meshtastic/core/service/RadioControllerImplTest.kt
similarity index 100%
rename from core/service/src/commonTest/kotlin/org/meshtastic/core/service/RadioControllerImplTest.kt
rename to core/service/src/nonWebTest/kotlin/org/meshtastic/core/service/RadioControllerImplTest.kt
diff --git a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/RadioControllerRestoreTest.kt b/core/service/src/nonWebTest/kotlin/org/meshtastic/core/service/RadioControllerRestoreTest.kt
similarity index 100%
rename from core/service/src/commonTest/kotlin/org/meshtastic/core/service/RadioControllerRestoreTest.kt
rename to core/service/src/nonWebTest/kotlin/org/meshtastic/core/service/RadioControllerRestoreTest.kt
diff --git a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/SharedRadioInterfaceServiceLivenessTest.kt b/core/service/src/nonWebTest/kotlin/org/meshtastic/core/service/SharedRadioInterfaceServiceLivenessTest.kt
similarity index 100%
rename from core/service/src/commonTest/kotlin/org/meshtastic/core/service/SharedRadioInterfaceServiceLivenessTest.kt
rename to core/service/src/nonWebTest/kotlin/org/meshtastic/core/service/SharedRadioInterfaceServiceLivenessTest.kt
diff --git a/core/service/src/wasmJsMain/kotlin/org/meshtastic/core/service/NoopTakServerIntegration.kt b/core/service/src/wasmJsMain/kotlin/org/meshtastic/core/service/NoopTakServerIntegration.kt
new file mode 100644
index 0000000000..f559ea2e33
--- /dev/null
+++ b/core/service/src/wasmJsMain/kotlin/org/meshtastic/core/service/NoopTakServerIntegration.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.meshtastic.core.service
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import org.koin.core.annotation.Single
+import org.meshtastic.core.repository.TakServerIntegration
+
+// Auto-discovered by CoreServiceModule's existing @ComponentScan("org.meshtastic.core.service") in this target's
+// compilation — no separate wasmJs Koin module needed (same "no per-target duplicate scan" reasoning as
+// core:database's SingleDatabaseProvider).
+//
+// core:takserver's TAK server is a raw TLS listener (SSLServerSocket) — a browser sandbox can never accept inbound
+// connections, so ATAK/TAK integration is a permanent web impossibility, not a pending feature. This is a real,
+// honest no-op, not a stand-in for future work.
+@Single(binds = [TakServerIntegration::class])
+internal class NoopTakServerIntegration : TakServerIntegration {
+ override val isRunning: StateFlow = MutableStateFlow(false)
+
+ override fun start(scope: CoroutineScope) = Unit
+
+ override fun stop() = Unit
+}
diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKMeshIntegration.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKMeshIntegration.kt
index d536a63288..566be00e9d 100644
--- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKMeshIntegration.kt
+++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/TAKMeshIntegration.kt
@@ -21,6 +21,7 @@ package org.meshtastic.core.takserver
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
@@ -34,6 +35,7 @@ import org.meshtastic.core.repository.MeshConfigHandler
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.repository.TakPrefs
+import org.meshtastic.core.repository.TakServerIntegration
import org.meshtastic.core.takserver.TAKPacketConversion.toCoTMessage
import org.meshtastic.core.takserver.TAKPacketConversion.toTAKPacket
import org.meshtastic.core.takserver.TAKPacketV2Conversion.toTAKPacketV2
@@ -98,8 +100,13 @@ class TAKMeshIntegration(
private val nodeRepository: NodeRepository,
private val meshToCotBroadcaster: MeshToCotBroadcaster,
private val takPrefs: TakPrefs,
-) {
- private val isRunning = AtomicBoolean(false)
+) : TakServerIntegration {
+ // This class's own start()/stop() re-entrancy latch (below) — distinct from isRunning, which reports
+ // the underlying TAK server's actual running state via TAKServerManager.
+ private val isRunningState = AtomicBoolean(false)
+
+ override val isRunning: StateFlow
+ get() = takServerManager.isRunning
// Immutable list reference replaced atomically in start()/stop(); never mutated in-place.
// @Volatile only guarantees visibility of the reference itself — any in-place mutation
@@ -115,8 +122,8 @@ class TAKMeshIntegration(
// from the single meshPacketFlow collector coroutine (handleMeshPacket), so no locking.
private val deliveryDedup = CotDeliveryDedup()
- fun start(scope: CoroutineScope) {
- if (!isRunning.compareAndSet(expectedValue = false, newValue = true)) return
+ override fun start(scope: CoroutineScope) {
+ if (!isRunningState.compareAndSet(expectedValue = false, newValue = true)) return
takServerManager.start(scope)
@@ -178,8 +185,8 @@ class TAKMeshIntegration(
Logger.i { "TAK Mesh Integration started — firmware=$fw, outbound=$proto" }
}
- fun stop() {
- if (!isRunning.compareAndSet(expectedValue = true, newValue = false)) return
+ override fun stop() {
+ if (!isRunningState.compareAndSet(expectedValue = true, newValue = false)) return
val toCancel = jobs
jobs = emptyList()
toCancel.forEach(Job::cancel)
diff --git a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/di/CoreTakServerModule.kt b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/di/CoreTakServerModule.kt
index 12836a92c9..12345a97c9 100644
--- a/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/di/CoreTakServerModule.kt
+++ b/core/takserver/src/commonMain/kotlin/org/meshtastic/core/takserver/di/CoreTakServerModule.kt
@@ -24,6 +24,7 @@ import org.meshtastic.core.repository.MeshConfigHandler
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.repository.TakPrefs
+import org.meshtastic.core.repository.TakServerIntegration
import org.meshtastic.core.takserver.MeshToCotBroadcaster
import org.meshtastic.core.takserver.TAKMeshIntegration
import org.meshtastic.core.takserver.TAKServer
@@ -46,7 +47,10 @@ class CoreTakServerModule {
dispatchers: CoroutineDispatchers,
): MeshToCotBroadcaster = MeshToCotBroadcaster(takServerManager, nodeRepository, takPrefs, dispatchers)
- @Single
+ // Bound under both types: feature/settings' debug TakMeshTestCard resolves the concrete TAKMeshIntegration
+ // (it needs members beyond TakServerIntegration's minimal seam), MeshServiceOrchestrator resolves
+ // TakServerIntegration (core:repository) so core:service never has to depend on core:takserver.
+ @Single(binds = [TAKMeshIntegration::class, TakServerIntegration::class])
fun provideTAKMeshIntegration(
takServerManager: TAKServerManager,
commandSender: CommandSender,