mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-09-14 14:21:32 -04:00
feat(service): enable wasmJs by extracting a TakServerIntegration seam
core:service is the central app-orchestration layer: MeshServiceOrchestrator
is constructed by both androidApp's MeshService.kt and desktopApp's
Main.kt, and will be needed by a future web client too -- it can't be
wholesale-excluded the way a leaf feature could be.
Its only web-hostile dependency was two constructor parameters typed
directly against core:takserver: TAKServerManager (already an
interface) and TAKMeshIntegration (a concrete class). core:takserver
can never get a wasmJs target -- its production implementation is a
TLS SSLServerSocket *listener* accepting inbound ATAK/iTAK
connections, and a browser sandbox can never accept inbound
connections at all, a more fundamental impossibility than the
outbound-only TCP case already excluded for MQTT.
MeshServiceOrchestrator only ever read takServerManager.isRunning and
called takMeshIntegration.start()/stop() -- the entire interaction
surface. Following this codebase's own convention (core:repository
hosts portable interfaces, core:*Impl-style modules hold the
platform-coupled implementation), a new minimal TakServerIntegration
interface in core:repository folds those three members into one seam.
TAKMeshIntegration now implements it directly; isRunning delegates to
the real takServerManager.isRunning (its own internal start/stop
re-entrancy latch, previously also named isRunning, is renamed to
isRunningState to keep the two states distinct). Its Koin provider
binds under both types (`@Single(binds = [TAKMeshIntegration::class,
TakServerIntegration::class])`) so feature/settings' debug UI still
resolves the concrete class while core:service resolves only the
interface -- verified sound against Koin's own K2-compiler-plugin
binding model via a real KoinVerificationTest run, not assumed.
core:service's dependency on core:takserver is removed entirely
(confirmed via grep: nothing else in the module referenced it).
wasmJs gets a real, honest no-op TakServerIntegration -- isRunning
always false, start/stop are no-ops -- documented as a permanent
platform impossibility, not a stand-in for future work.
androidApp/desktopApp needed zero changes: both already register
core:takserver's own Koin module directly, which still supplies the
real implementation there.
Three of six commonTest files move to a new nonWebTest source set,
for two unrelated, both-confirmed-empirically reasons:
SharedRadioInterfaceServiceLivenessTest.kt depends on core:testing (no
wasmJs target, the same gap every KMP module's test suite has hit this
session); RadioControllerImplTest.kt and RadioControllerRestoreTest.kt
crash the Kotlin/Wasm compiler ("Serialization of IrErrorType is not
supported anymore") when constructing a real RadioControllerImpl --
bisected to exactly these two files, which both differ from the four
that pass by constructing that class (interface delegation via `by`
plus Lazy<T> constructor params) -- a genuine backend limitation, not
a library gap, logged as deferred rather than worked around.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
58d1d5068c
commit
c3d19fb829
10 files changed
+182
-73
No files matched your search
+37
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<Boolean>
|
||||
|
||||
/** 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()
|
||||
}
|
||||
@@ -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<T>/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) }
|
||||
}
|
||||
}
|
||||
+6
-8
@@ -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
|
||||
|
||||
+44
-56
@@ -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<String>()
|
||||
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<String>()
|
||||
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)
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.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<Boolean> = MutableStateFlow(false)
|
||||
|
||||
override fun start(scope: CoroutineScope) = Unit
|
||||
|
||||
override fun stop() = Unit
|
||||
}
|
||||
+13
-6
@@ -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<Boolean>
|
||||
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)
|
||||
|
||||
+5
-1
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user