From 174315b21f5affcb952642f1650d316c86b8a40f Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Sat, 11 Apr 2026 18:39:29 -0500 Subject: [PATCH] refactor(data): replace lateinit var scope + start() with constructor injection (#5075) --- .../core/data/manager/CommandSenderImpl.kt | 6 +- .../data/manager/MeshActionHandlerImpl.kt | 7 +- .../data/manager/MeshConfigFlowManagerImpl.kt | 7 +- .../data/manager/MeshConfigHandlerImpl.kt | 6 +- .../data/manager/MeshConnectionManagerImpl.kt | 11 +-- .../core/data/manager/MeshDataHandlerImpl.kt | 9 +- .../data/manager/MeshMessageProcessorImpl.kt | 6 +- .../core/data/manager/MeshRouterImpl.kt | 10 -- .../core/data/manager/MqttManagerImpl.kt | 6 +- .../data/manager/NeighborInfoHandlerImpl.kt | 6 -- .../core/data/manager/NodeManagerImpl.kt | 7 +- .../core/data/manager/PacketHandlerImpl.kt | 8 +- .../manager/StoreForwardPacketHandlerImpl.kt | 7 +- .../manager/TelemetryPacketHandlerImpl.kt | 7 +- .../data/manager/TracerouteHandlerImpl.kt | 7 +- .../data/manager/MeshActionHandlerImplTest.kt | 99 ++++++++++--------- .../manager/MeshConfigFlowManagerImplTest.kt | 2 +- .../data/manager/MeshConfigHandlerImplTest.kt | 37 +++---- .../manager/MeshConnectionManagerImplTest.kt | 64 ++++++------ .../core/data/manager/MeshDataHandlerTest.kt | 2 +- .../manager/MeshMessageProcessorImplTest.kt | 47 ++++----- .../core/data/manager/NodeManagerImplTest.kt | 4 +- .../data/manager/PacketHandlerImplTest.kt | 2 +- .../StoreForwardPacketHandlerImplTest.kt | 2 +- .../manager/TelemetryPacketHandlerImplTest.kt | 2 +- .../core/repository/CommandSender.kt | 4 - .../core/repository/MeshActionHandler.kt | 4 - .../core/repository/MeshConfigFlowManager.kt | 4 - .../core/repository/MeshConfigHandler.kt | 4 - .../core/repository/MeshConnectionManager.kt | 4 - .../core/repository/MeshDataHandler.kt | 4 - .../core/repository/MeshMessageProcessor.kt | 4 - .../meshtastic/core/repository/MeshRouter.kt | 5 - .../meshtastic/core/repository/MqttManager.kt | 5 +- .../core/repository/NeighborInfoHandler.kt | 4 - .../meshtastic/core/repository/NodeManager.kt | 4 - .../core/repository/PacketHandler.kt | 4 - .../repository/StoreForwardPacketHandler.kt | 4 - .../core/repository/TelemetryPacketHandler.kt | 4 - .../core/repository/TracerouteHandler.kt | 4 - .../core/service/MeshServiceOrchestrator.kt | 29 ++---- .../core/service/di/CoreServiceModule.kt | 12 ++- .../service/MeshServiceOrchestratorTest.kt | 15 +-- 43 files changed, 188 insertions(+), 301 deletions(-) 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 c26dc0f5f..ca22f927d 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 @@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import okio.ByteString import okio.ByteString.Companion.toByteString +import org.koin.core.annotation.Named import org.koin.core.annotation.Single import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.model.DataPacket @@ -59,8 +60,8 @@ class CommandSenderImpl( private val radioConfigRepository: RadioConfigRepository, private val tracerouteHandler: TracerouteHandler, private val neighborInfoHandler: NeighborInfoHandler, + @Named("ServiceScope") private val scope: CoroutineScope, ) : CommandSender { - private lateinit var scope: CoroutineScope private val currentPacketId = atomic(Random(nowMillis).nextLong().absoluteValue) private val sessionPasskey = atomic(ByteString.EMPTY) @@ -71,8 +72,7 @@ class CommandSenderImpl( // maybe via ServiceRepository or similar. // For now I'll assume it's injected or available. - override fun start(scope: CoroutineScope) { - this.scope = scope + init { radioConfigRepository.localConfigFlow.onEach { localConfig.value = it }.launchIn(scope) radioConfigRepository.channelSetFlow.onEach { channelSet.value = it }.launchIn(scope) } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt index 027947453..7f9e6c3fa 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt @@ -19,6 +19,7 @@ package org.meshtastic.core.data.manager import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope import okio.ByteString.Companion.toByteString +import org.koin.core.annotation.Named import org.koin.core.annotation.Single import org.meshtastic.core.common.database.DatabaseManager import org.meshtastic.core.common.util.handledLaunch @@ -64,12 +65,8 @@ class MeshActionHandlerImpl( private val notificationManager: NotificationManager, private val messageProcessor: Lazy, private val radioConfigRepository: RadioConfigRepository, + @Named("ServiceScope") private val scope: CoroutineScope, ) : MeshActionHandler { - private lateinit var scope: CoroutineScope - - override fun start(scope: CoroutineScope) { - this.scope = scope - } companion object { private const val DEFAULT_REBOOT_DELAY = 5 diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt index dc544a300..b7b27aa4e 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt @@ -21,6 +21,7 @@ import kotlinx.atomicfu.atomic import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import okio.IOException +import org.koin.core.annotation.Named import org.koin.core.annotation.Single import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.model.ConnectionState @@ -56,17 +57,13 @@ class MeshConfigFlowManagerImpl( private val analytics: PlatformAnalytics, private val commandSender: CommandSender, private val packetHandler: PacketHandler, + @Named("ServiceScope") private val scope: CoroutineScope, ) : MeshConfigFlowManager { - private lateinit var scope: CoroutineScope private val wantConfigDelay = 100L /** Monotonically increasing generation so async clears from a stale handshake are discarded. */ private val handshakeGeneration = atomic(0L) - override fun start(scope: CoroutineScope) { - this.scope = scope - } - /** * Type-safe handshake state machine. Each state carries exactly the data that is valid during that phase, * eliminating the possibility of accessing stale or uninitialized fields. diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt index 06d973204..b622cedbf 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import org.koin.core.annotation.Named import org.koin.core.annotation.Single import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.repository.MeshConfigHandler @@ -40,8 +41,8 @@ class MeshConfigHandlerImpl( private val radioConfigRepository: RadioConfigRepository, private val serviceRepository: ServiceRepository, private val nodeManager: NodeManager, + @Named("ServiceScope") private val scope: CoroutineScope, ) : MeshConfigHandler { - private lateinit var scope: CoroutineScope private val _localConfig = MutableStateFlow(LocalConfig()) override val localConfig = _localConfig.asStateFlow() @@ -49,8 +50,7 @@ class MeshConfigHandlerImpl( private val _moduleConfig = MutableStateFlow(LocalModuleConfig()) override val moduleConfig = _moduleConfig.asStateFlow() - override fun start(scope: CoroutineScope) { - this.scope = scope + init { radioConfigRepository.localConfigFlow.onEach { _localConfig.value = it }.launchIn(scope) radioConfigRepository.moduleConfigFlow.onEach { _moduleConfig.value = it }.launchIn(scope) } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt index 5954b579c..fde8841ce 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt @@ -19,13 +19,13 @@ package org.meshtastic.core.data.manager import co.touchlab.kermit.Logger import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import org.koin.core.annotation.Named import org.koin.core.annotation.Single import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.common.util.nowMillis @@ -81,17 +81,15 @@ class MeshConnectionManagerImpl( private val packetRepository: PacketRepository, private val workerManager: MeshWorkerManager, private val appWidgetUpdater: AppWidgetUpdater, + @Named("ServiceScope") private val scope: CoroutineScope, ) : MeshConnectionManager { - private lateinit var scope: CoroutineScope private var sleepTimeout: Job? = null private var locationRequestsJob: Job? = null private var handshakeTimeout: Job? = null private var connectTimeMsec = 0L private var connectionRestored = false - @OptIn(FlowPreview::class) - override fun start(scope: CoroutineScope) { - this.scope = scope + init { radioInterfaceService.connectionState.onEach(::onRadioConnectionState).launchIn(scope) // Ensure notification title and content stay in sync with state changes @@ -302,8 +300,7 @@ class MeshConnectionManagerImpl( // Start MQTT if enabled scope.handledLaunch { val moduleConfig = radioConfigRepository.moduleConfigFlow.first() - mqttManager.start( - scope, + mqttManager.startProxy( moduleConfig.mqtt?.enabled == true, moduleConfig.mqtt?.proxy_to_client_enabled == true, ) diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt index 07521b21c..5da0448b5 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import org.koin.core.annotation.Named import org.koin.core.annotation.Single import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.common.util.nowMillis @@ -94,14 +95,8 @@ class MeshDataHandlerImpl( private val storeForwardHandler: StoreForwardPacketHandler, private val telemetryHandler: TelemetryPacketHandler, private val adminPacketHandler: AdminPacketHandler, + @Named("ServiceScope") private val scope: CoroutineScope, ) : MeshDataHandler { - private lateinit var scope: CoroutineScope - - override fun start(scope: CoroutineScope) { - this.scope = scope - storeForwardHandler.start(scope) - telemetryHandler.start(scope) - } private val rememberDataType = setOf( diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt index 9fd28ecb4..288ae9645 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import org.koin.core.annotation.Named import org.koin.core.annotation.Single import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.common.util.nowMillis @@ -53,8 +54,8 @@ class MeshMessageProcessorImpl( private val meshLogRepository: Lazy, private val router: Lazy, private val fromRadioDispatcher: FromRadioPacketHandler, + @Named("ServiceScope") private val scope: CoroutineScope, ) : MeshMessageProcessor { - private lateinit var scope: CoroutineScope private val mapsMutex = Mutex() private val logUuidByPacketId = mutableMapOf() @@ -75,8 +76,7 @@ class MeshMessageProcessorImpl( scope.launch { earlyMutex.withLock { earlyReceivedPackets.clear() } } } - override fun start(scope: CoroutineScope) { - this.scope = scope + init { nodeManager.isNodeDbReady .onEach { ready -> if (ready) { diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.kt index aaf109be9..8973589bd 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.kt @@ -16,7 +16,6 @@ */ package org.meshtastic.core.data.manager -import kotlinx.coroutines.CoroutineScope import org.koin.core.annotation.Single import org.meshtastic.core.repository.MeshActionHandler import org.meshtastic.core.repository.MeshConfigFlowManager @@ -64,13 +63,4 @@ class MeshRouterImpl( override val xmodemManager: XModemManager get() = xmodemManagerLazy.value - - override fun start(scope: CoroutineScope) { - dataHandler.start(scope) - configHandler.start(scope) - tracerouteHandler.start(scope) - neighborInfoHandler.start(scope) - configFlowManager.start(scope) - actionHandler.start(scope) - } } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt index 969b67a2f..b928e8505 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import org.koin.core.annotation.Named import org.koin.core.annotation.Single import org.meshtastic.core.network.repository.MQTTRepository import org.meshtastic.core.repository.MqttManager @@ -36,12 +37,11 @@ class MqttManagerImpl( private val mqttRepository: MQTTRepository, private val packetHandler: PacketHandler, private val serviceRepository: ServiceRepository, + @Named("ServiceScope") private val scope: CoroutineScope, ) : MqttManager { - private lateinit var scope: CoroutineScope private var mqttMessageFlow: Job? = null - override fun start(scope: CoroutineScope, enabled: Boolean, proxyToClientEnabled: Boolean) { - this.scope = scope + override fun startProxy(enabled: Boolean, proxyToClientEnabled: Boolean) { if (mqttMessageFlow?.isActive == true) return if (enabled && proxyToClientEnabled) { mqttMessageFlow = diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt index 4019e5a9b..3f483ba25 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt @@ -20,7 +20,6 @@ import co.touchlab.kermit.Logger import kotlinx.atomicfu.atomic import kotlinx.atomicfu.update import kotlinx.collections.immutable.persistentMapOf -import kotlinx.coroutines.CoroutineScope import org.koin.core.annotation.Single import org.meshtastic.core.common.util.NumberFormatter import org.meshtastic.core.common.util.nowMillis @@ -37,16 +36,11 @@ class NeighborInfoHandlerImpl( private val serviceRepository: ServiceRepository, private val serviceBroadcasts: ServiceBroadcasts, ) : NeighborInfoHandler { - private lateinit var scope: CoroutineScope private val startTimes = atomic(persistentMapOf()) override var lastNeighborInfo: NeighborInfo? = null - override fun start(scope: CoroutineScope) { - this.scope = scope - } - override fun recordStartTime(requestId: Int) { startTimes.update { it.put(requestId, nowMillis) } } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt index 9ce4ba05d..fe6d22f4c 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import okio.ByteString +import org.koin.core.annotation.Named import org.koin.core.annotation.Single import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.model.DataPacket @@ -59,8 +60,8 @@ class NodeManagerImpl( private val nodeRepository: NodeRepository, private val serviceBroadcasts: ServiceBroadcasts, private val notificationManager: NotificationManager, + @Named("ServiceScope") private val scope: CoroutineScope, ) : NodeManager { - private lateinit var scope: CoroutineScope private val _nodeDBbyNodeNum = atomic(persistentMapOf()) private val _nodeDBbyID = atomic(persistentMapOf()) @@ -88,10 +89,6 @@ class NodeManagerImpl( myNodeNum.value = num } - override fun start(scope: CoroutineScope) { - this.scope = scope - } - companion object { private const val TIME_MS_TO_S = 1000L } diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt index 1d4d11adc..7c634ee8b 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt @@ -28,6 +28,7 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeoutOrNull +import org.koin.core.annotation.Named import org.koin.core.annotation.Single import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.common.util.nowMillis @@ -60,6 +61,7 @@ class PacketHandlerImpl( private val radioInterfaceService: RadioInterfaceService, private val meshLogRepository: Lazy, private val serviceRepository: ServiceRepository, + @Named("ServiceScope") private val scope: CoroutineScope, ) : PacketHandler { companion object { @@ -67,7 +69,6 @@ class PacketHandlerImpl( } private var queueJob: Job? = null - private lateinit var scope: CoroutineScope private val queueMutex = Mutex() private val queuedPackets = mutableListOf() @@ -79,11 +80,6 @@ class PacketHandlerImpl( private val responseMutex = Mutex() private val queueResponse = mutableMapOf>() - override fun start(scope: CoroutineScope) { - this.scope = scope - queueStopped = false // Safe: called before any concurrent operations on this scope. - } - override fun sendToRadio(p: ToRadio) { Logger.d { "Sending to radio ${p.toPIIString()}" } val b = p.encode() diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImpl.kt index 4f71879ce..e8ab4eeb7 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImpl.kt @@ -20,6 +20,7 @@ import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope import okio.ByteString.Companion.toByteString import okio.IOException +import org.koin.core.annotation.Named import org.koin.core.annotation.Single import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.model.DataPacket @@ -45,12 +46,8 @@ class StoreForwardPacketHandlerImpl( private val serviceBroadcasts: ServiceBroadcasts, private val historyManager: HistoryManager, private val dataHandler: Lazy, + @Named("ServiceScope") private val scope: CoroutineScope, ) : StoreForwardPacketHandler { - private lateinit var scope: CoroutineScope - - override fun start(scope: CoroutineScope) { - this.scope = scope - } override fun handleStoreAndForward(packet: MeshPacket, dataPacket: DataPacket, myNodeNum: Int) { val payload = packet.decoded?.payload ?: return diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TelemetryPacketHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TelemetryPacketHandlerImpl.kt index 205dd30e2..4887ff19b 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TelemetryPacketHandlerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TelemetryPacketHandlerImpl.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import org.koin.core.annotation.Named import org.koin.core.annotation.Single import org.meshtastic.core.common.util.nowSeconds import org.meshtastic.core.model.DataPacket @@ -49,16 +50,12 @@ class TelemetryPacketHandlerImpl( private val nodeManager: NodeManager, private val connectionManager: Lazy, private val notificationManager: NotificationManager, + @Named("ServiceScope") private val scope: CoroutineScope, ) : TelemetryPacketHandler { - private lateinit var scope: CoroutineScope private val batteryMutex = Mutex() private val batteryPercentCooldowns = mutableMapOf() - override fun start(scope: CoroutineScope) { - this.scope = scope - } - @Suppress("LongMethod", "CyclomaticComplexMethod") override fun handleTelemetry(packet: MeshPacket, dataPacket: DataPacket, myNodeNum: Int) { val payload = packet.decoded?.payload ?: return diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt index 5e8d954f6..a5997208b 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt @@ -22,6 +22,7 @@ import kotlinx.atomicfu.update import kotlinx.collections.immutable.persistentMapOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import org.koin.core.annotation.Named import org.koin.core.annotation.Single import org.meshtastic.core.common.util.NumberFormatter import org.meshtastic.core.common.util.handledLaunch @@ -42,15 +43,11 @@ class TracerouteHandlerImpl( private val serviceRepository: ServiceRepository, private val tracerouteSnapshotRepository: TracerouteSnapshotRepository, private val nodeRepository: NodeRepository, + @Named("ServiceScope") private val scope: CoroutineScope, ) : TracerouteHandler { - private lateinit var scope: CoroutineScope private val startTimes = atomic(persistentMapOf()) - override fun start(scope: CoroutineScope) { - this.scope = scope - } - override fun recordStartTime(requestId: Int) { startTimes.update { it.put(requestId, nowMillis) } } diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImplTest.kt index 6ac094e48..c53c2577e 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImplTest.kt @@ -25,6 +25,7 @@ import dev.mokkery.mock import dev.mokkery.verify import dev.mokkery.verify.VerifyMode.Companion.not import dev.mokkery.verifySuspend +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -89,28 +90,28 @@ class MeshActionHandlerImplTest { every { nodeManager.myNodeNum } returns myNodeNumFlow every { nodeManager.getMyId() } returns "!12345678" every { nodeManager.nodeDBbyNodeNum } returns emptyMap() - - handler = - MeshActionHandlerImpl( - nodeManager = nodeManager, - commandSender = commandSender, - packetRepository = lazy { packetRepository }, - serviceBroadcasts = serviceBroadcasts, - dataHandler = lazy { dataHandler }, - analytics = analytics, - meshPrefs = meshPrefs, - databaseManager = databaseManager, - notificationManager = notificationManager, - messageProcessor = lazy { messageProcessor }, - radioConfigRepository = radioConfigRepository, - ) } + private fun createHandler(scope: CoroutineScope): MeshActionHandlerImpl = MeshActionHandlerImpl( + nodeManager = nodeManager, + commandSender = commandSender, + packetRepository = lazy { packetRepository }, + serviceBroadcasts = serviceBroadcasts, + dataHandler = lazy { dataHandler }, + analytics = analytics, + meshPrefs = meshPrefs, + databaseManager = databaseManager, + notificationManager = notificationManager, + messageProcessor = lazy { messageProcessor }, + radioConfigRepository = radioConfigRepository, + scope = scope, + ) + // ---- handleUpdateLastAddress (device-switch path — P0 critical) ---- @Test fun handleUpdateLastAddress_differentAddress_switchesDatabaseAndClearsState() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) every { meshPrefs.deviceAddress } returns MutableStateFlow("old_addr") everySuspend { databaseManager.switchActiveDatabase(any()) } returns Unit @@ -128,7 +129,7 @@ class MeshActionHandlerImplTest { @Test fun handleUpdateLastAddress_sameAddress_noOp() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) every { meshPrefs.deviceAddress } returns MutableStateFlow("same_addr") @@ -141,7 +142,7 @@ class MeshActionHandlerImplTest { @Test fun handleUpdateLastAddress_nullAddress_switchesIfDifferent() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) every { meshPrefs.deviceAddress } returns MutableStateFlow("old_addr") everySuspend { databaseManager.switchActiveDatabase(any()) } returns Unit @@ -156,7 +157,7 @@ class MeshActionHandlerImplTest { @Test fun handleUpdateLastAddress_nullToNull_noOp() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) every { meshPrefs.deviceAddress } returns MutableStateFlow(null) @@ -168,7 +169,7 @@ class MeshActionHandlerImplTest { @Test fun handleUpdateLastAddress_executesStepsInOrder() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) every { meshPrefs.deviceAddress } returns MutableStateFlow("old") everySuspend { databaseManager.switchActiveDatabase(any()) } returns Unit @@ -187,7 +188,7 @@ class MeshActionHandlerImplTest { @Test fun onServiceAction_nullMyNodeNum_doesNothing() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) myNodeNumFlow.value = null val node = createTestNode(REMOTE_NODE_NUM) @@ -201,7 +202,7 @@ class MeshActionHandlerImplTest { @Test fun onServiceAction_favorite_sendsSetFavoriteWhenNotFavorite() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) val node = createTestNode(REMOTE_NODE_NUM, isFavorite = false) handler.onServiceAction(ServiceAction.Favorite(node)) @@ -213,7 +214,7 @@ class MeshActionHandlerImplTest { @Test fun onServiceAction_favorite_sendsRemoveFavoriteWhenAlreadyFavorite() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) val node = createTestNode(REMOTE_NODE_NUM, isFavorite = true) handler.onServiceAction(ServiceAction.Favorite(node)) @@ -227,7 +228,7 @@ class MeshActionHandlerImplTest { @Test fun onServiceAction_ignore_togglesAndUpdatesFilteredBySender() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) val node = createTestNode(REMOTE_NODE_NUM, isIgnored = false) handler.onServiceAction(ServiceAction.Ignore(node)) @@ -242,7 +243,7 @@ class MeshActionHandlerImplTest { @Test fun onServiceAction_mute_togglesMutedState() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) val node = createTestNode(REMOTE_NODE_NUM, isMuted = false) handler.onServiceAction(ServiceAction.Mute(node)) @@ -256,7 +257,7 @@ class MeshActionHandlerImplTest { @Test fun onServiceAction_getDeviceMetadata_sendsAdminRequest() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) handler.onServiceAction(ServiceAction.GetDeviceMetadata(REMOTE_NODE_NUM)) advanceUntilIdle() @@ -268,7 +269,7 @@ class MeshActionHandlerImplTest { @Test fun onServiceAction_sendContact_completesWithTrueOnSuccess() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) everySuspend { commandSender.sendAdminAwait(any(), any(), any(), any()) } returns true val action = ServiceAction.SendContact(SharedContact()) @@ -281,7 +282,7 @@ class MeshActionHandlerImplTest { @Test fun onServiceAction_sendContact_completesWithFalseOnFailure() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) everySuspend { commandSender.sendAdminAwait(any(), any(), any(), any()) } returns false val action = ServiceAction.SendContact(SharedContact()) @@ -296,7 +297,7 @@ class MeshActionHandlerImplTest { @Test fun onServiceAction_importContact_sendsAdminAndUpdatesNode() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) val contact = SharedContact(node_num = REMOTE_NODE_NUM, user = User(id = "!abcdef12", long_name = "TestUser")) @@ -311,7 +312,7 @@ class MeshActionHandlerImplTest { @Test fun handleSetOwner_sendsAdminAndUpdatesLocalNode() { - handler.start(testScope) + handler = createHandler(testScope) val meshUser = MeshUser( id = "!12345678", @@ -331,7 +332,7 @@ class MeshActionHandlerImplTest { @Test fun handleSend_sendsDataAndBroadcastsStatus() { - handler.start(testScope) + handler = createHandler(testScope) val packet = DataPacket(to = "!deadbeef", dataType = 1, bytes = null, channel = 0) handler.handleSend(packet, MY_NODE_NUM) @@ -345,7 +346,7 @@ class MeshActionHandlerImplTest { @Test fun handleRequestPosition_sameNode_doesNothing() { - handler.start(testScope) + handler = createHandler(testScope) handler.handleRequestPosition(MY_NODE_NUM, Position(0.0, 0.0, 0), MY_NODE_NUM) @@ -354,7 +355,7 @@ class MeshActionHandlerImplTest { @Test fun handleRequestPosition_provideLocation_validPosition_usesGivenPosition() { - handler.start(testScope) + handler = createHandler(testScope) every { meshPrefs.shouldProvideNodeLocation(MY_NODE_NUM) } returns MutableStateFlow(true) val validPosition = Position(37.7749, -122.4194, 10) @@ -365,7 +366,7 @@ class MeshActionHandlerImplTest { @Test fun handleRequestPosition_provideLocation_invalidPosition_fallsBackToNodeDB() { - handler.start(testScope) + handler = createHandler(testScope) every { meshPrefs.shouldProvideNodeLocation(MY_NODE_NUM) } returns MutableStateFlow(true) every { nodeManager.nodeDBbyNodeNum } returns emptyMap() @@ -378,7 +379,7 @@ class MeshActionHandlerImplTest { @Test fun handleRequestPosition_doNotProvide_sendsZeroPosition() { - handler.start(testScope) + handler = createHandler(testScope) every { meshPrefs.shouldProvideNodeLocation(MY_NODE_NUM) } returns MutableStateFlow(false) val validPosition = Position(37.7749, -122.4194, 10) @@ -392,7 +393,7 @@ class MeshActionHandlerImplTest { @Test fun handleSetConfig_decodesAndSendsAdmin_thenPersistsLocally() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) everySuspend { radioConfigRepository.setLocalConfig(any()) } returns Unit val config = Config(lora = Config.LoRaConfig(hop_limit = 5)) @@ -409,7 +410,7 @@ class MeshActionHandlerImplTest { @Test fun handleSetModuleConfig_ownNode_persistsLocally() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) myNodeNumFlow.value = MY_NODE_NUM everySuspend { radioConfigRepository.setLocalModuleConfig(any()) } returns Unit @@ -425,7 +426,7 @@ class MeshActionHandlerImplTest { @Test fun handleSetModuleConfig_remoteNode_doesNotPersistLocally() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) myNodeNumFlow.value = MY_NODE_NUM val moduleConfig = ModuleConfig(mqtt = ModuleConfig.MQTTConfig(enabled = true)) @@ -442,7 +443,7 @@ class MeshActionHandlerImplTest { @Test fun handleSetChannel_nonNullPayload_decodesAndPersists() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) everySuspend { radioConfigRepository.updateChannelSettings(any()) } returns Unit val channel = Channel(index = 1) @@ -457,7 +458,7 @@ class MeshActionHandlerImplTest { @Test fun handleSetChannel_nullPayload_doesNothing() { - handler.start(testScope) + handler = createHandler(testScope) handler.handleSetChannel(null, MY_NODE_NUM) @@ -468,7 +469,7 @@ class MeshActionHandlerImplTest { @Test fun handleRemoveByNodenum_removesAndSendsAdmin() { - handler.start(testScope) + handler = createHandler(testScope) handler.handleRemoveByNodenum(REMOTE_NODE_NUM, 99, MY_NODE_NUM) @@ -480,7 +481,7 @@ class MeshActionHandlerImplTest { @Test fun handleSetRemoteOwner_decodesAndSendsAdmin() { - handler.start(testScope) + handler = createHandler(testScope) val user = User(id = "!remote01", long_name = "Remote", short_name = "RM") val payload = User.ADAPTER.encode(user) @@ -495,7 +496,7 @@ class MeshActionHandlerImplTest { @Test fun handleGetRemoteConfig_sessionkeyConfig_sendsDeviceMetadataRequest() { - handler.start(testScope) + handler = createHandler(testScope) handler.handleGetRemoteConfig(1, REMOTE_NODE_NUM, AdminMessage.ConfigType.SESSIONKEY_CONFIG.value) @@ -504,7 +505,7 @@ class MeshActionHandlerImplTest { @Test fun handleGetRemoteConfig_regularConfig_sendsConfigRequest() { - handler.start(testScope) + handler = createHandler(testScope) handler.handleGetRemoteConfig(1, REMOTE_NODE_NUM, AdminMessage.ConfigType.LORA_CONFIG.value) @@ -515,7 +516,7 @@ class MeshActionHandlerImplTest { @Test fun handleSetRemoteChannel_nullPayload_doesNothing() { - handler.start(testScope) + handler = createHandler(testScope) handler.handleSetRemoteChannel(1, REMOTE_NODE_NUM, null) @@ -524,7 +525,7 @@ class MeshActionHandlerImplTest { @Test fun handleSetRemoteChannel_nonNullPayload_decodesAndSendsAdmin() { - handler.start(testScope) + handler = createHandler(testScope) val channel = Channel(index = 2) val payload = Channel.ADAPTER.encode(channel) @@ -538,7 +539,7 @@ class MeshActionHandlerImplTest { @Test fun handleRequestRebootOta_withNullHash_sendsAdmin() { - handler.start(testScope) + handler = createHandler(testScope) handler.handleRequestRebootOta(1, REMOTE_NODE_NUM, 0, null) @@ -547,7 +548,7 @@ class MeshActionHandlerImplTest { @Test fun handleRequestRebootOta_withHash_sendsAdmin() { - handler.start(testScope) + handler = createHandler(testScope) val hash = byteArrayOf(0x01, 0x02, 0x03) handler.handleRequestRebootOta(1, REMOTE_NODE_NUM, 1, hash) @@ -559,7 +560,7 @@ class MeshActionHandlerImplTest { @Test fun handleRequestNodedbReset_sendsAdminWithPreserveFavorites() { - handler.start(testScope) + handler = createHandler(testScope) handler.handleRequestNodedbReset(1, REMOTE_NODE_NUM, preserveFavorites = true) diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImplTest.kt index 9580d5363..e05c6f20a 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImplTest.kt @@ -98,8 +98,8 @@ class MeshConfigFlowManagerImplTest { analytics = analytics, commandSender = commandSender, packetHandler = packetHandler, + scope = testScope, ) - manager.start(testScope) } // ---------- handleMyInfo ---------- diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImplTest.kt index b71942d0e..bf3247815 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImplTest.kt @@ -23,6 +23,7 @@ import dev.mokkery.matcher.any import dev.mokkery.mock import dev.mokkery.verify import dev.mokkery.verifySuspend +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -60,20 +61,20 @@ class MeshConfigHandlerImplTest { fun setUp() { every { radioConfigRepository.localConfigFlow } returns localConfigFlow every { radioConfigRepository.moduleConfigFlow } returns moduleConfigFlow - - handler = - MeshConfigHandlerImpl( - radioConfigRepository = radioConfigRepository, - serviceRepository = serviceRepository, - nodeManager = nodeManager, - ) } + private fun createHandler(scope: CoroutineScope): MeshConfigHandlerImpl = MeshConfigHandlerImpl( + radioConfigRepository = radioConfigRepository, + serviceRepository = serviceRepository, + nodeManager = nodeManager, + scope = scope, + ) + // ---------- start and flow wiring ---------- @Test fun `start wires localConfig flow from repository`() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) val config = LocalConfig(device = Config.DeviceConfig(role = Config.DeviceConfig.Role.ROUTER)) localConfigFlow.value = config advanceUntilIdle() @@ -83,7 +84,7 @@ class MeshConfigHandlerImplTest { @Test fun `start wires moduleConfig flow from repository`() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) val config = LocalModuleConfig(mqtt = ModuleConfig.MQTTConfig(enabled = true)) moduleConfigFlow.value = config advanceUntilIdle() @@ -95,7 +96,7 @@ class MeshConfigHandlerImplTest { @Test fun `handleDeviceConfig persists config and updates progress`() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) val config = Config(device = Config.DeviceConfig(role = Config.DeviceConfig.Role.CLIENT)) handler.handleDeviceConfig(config) advanceUntilIdle() @@ -106,7 +107,7 @@ class MeshConfigHandlerImplTest { @Test fun `handleDeviceConfig handles all config variants`() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) val configs = listOf( Config(position = Config.PositionConfig()), @@ -131,7 +132,7 @@ class MeshConfigHandlerImplTest { @Test fun `handleModuleConfig persists config and updates progress`() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) val config = ModuleConfig(mqtt = ModuleConfig.MQTTConfig(enabled = true)) handler.handleModuleConfig(config) advanceUntilIdle() @@ -142,7 +143,7 @@ class MeshConfigHandlerImplTest { @Test fun `handleModuleConfig with statusmessage updates node status`() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) val myNum = 123 every { nodeManager.myNodeNum } returns MutableStateFlow(myNum) @@ -155,7 +156,7 @@ class MeshConfigHandlerImplTest { @Test fun `handleModuleConfig with statusmessage skipped when myNodeNum is null`() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) every { nodeManager.myNodeNum } returns MutableStateFlow(null) val config = ModuleConfig(statusmessage = ModuleConfig.StatusMessageConfig(node_status = "Active")) @@ -168,7 +169,7 @@ class MeshConfigHandlerImplTest { @Test fun `handleChannel persists channel settings`() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) val channel = Channel(index = 0) handler.handleChannel(channel) advanceUntilIdle() @@ -178,7 +179,7 @@ class MeshConfigHandlerImplTest { @Test fun `handleChannel shows progress with max channels when myNodeInfo available`() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) every { nodeManager.getMyNodeInfo() } returns MyNodeInfo( myNodeNum = 123, @@ -206,7 +207,7 @@ class MeshConfigHandlerImplTest { @Test fun `handleChannel shows progress without max channels when myNodeInfo unavailable`() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) every { nodeManager.getMyNodeInfo() } returns null val channel = Channel(index = 0) @@ -220,7 +221,7 @@ class MeshConfigHandlerImplTest { @Test fun `handleDeviceUIConfig persists config`() = runTest(testDispatcher) { - handler.start(backgroundScope) + handler = createHandler(backgroundScope) val config = DeviceUIConfig() handler.handleDeviceUIConfig(config) advanceUntilIdle() diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt index 5263254d3..36ee37f2e 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt @@ -24,7 +24,7 @@ import dev.mokkery.everySuspend import dev.mokkery.matcher.any import dev.mokkery.mock import dev.mokkery.verify -import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -60,7 +60,7 @@ import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals -@OptIn(ExperimentalCoroutinesApi::class) +@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) class MeshConnectionManagerImplTest { private val radioInterfaceService = mock(MockMode.autofill) private val serviceRepository = mock(MockMode.autofill) @@ -108,29 +108,29 @@ class MeshConnectionManagerImplTest { every { mqttManager.stop() } returns Unit every { nodeManager.nodeDBbyNodeNum } returns emptyMap() every { packetHandler.sendToRadio(any()) } returns Unit - - manager = - MeshConnectionManagerImpl( - radioInterfaceService, - serviceRepository, - serviceBroadcasts, - serviceNotifications, - uiPrefs, - packetHandler, - nodeRepository, - locationManager, - mqttManager, - historyManager, - radioConfigRepository, - commandSender, - nodeManager, - analytics, - packetRepository, - workerManager, - appWidgetUpdater, - ) } + private fun createManager(scope: CoroutineScope): MeshConnectionManagerImpl = MeshConnectionManagerImpl( + radioInterfaceService, + serviceRepository, + serviceBroadcasts, + serviceNotifications, + uiPrefs, + packetHandler, + nodeRepository, + locationManager, + mqttManager, + historyManager, + radioConfigRepository, + commandSender, + nodeManager, + analytics, + packetRepository, + workerManager, + appWidgetUpdater, + scope, + ) + @AfterTest fun tearDown() {} @Test @@ -138,7 +138,7 @@ class MeshConnectionManagerImplTest { every { packetHandler.sendToRadio(any()) } returns Unit every { serviceNotifications.updateServiceStateNotification(any(), any()) } returns Unit - manager.start(backgroundScope) + manager = createManager(backgroundScope) radioConnectionState.value = ConnectionState.Connected advanceUntilIdle() @@ -163,7 +163,7 @@ class MeshConnectionManagerImplTest { every { locationManager.stop() } returns Unit every { mqttManager.stop() } returns Unit every { nodeManager.nodeDBbyNodeNum } returns emptyMap() - manager.start(backgroundScope) + manager = createManager(backgroundScope) // Transition to Connected first so that Disconnected actually does something radioConnectionState.value = ConnectionState.Connected advanceUntilIdle() @@ -197,7 +197,7 @@ class MeshConnectionManagerImplTest { every { mqttManager.stop() } returns Unit every { nodeManager.nodeDBbyNodeNum } returns emptyMap() - manager.start(backgroundScope) + manager = createManager(backgroundScope) advanceUntilIdle() radioConnectionState.value = ConnectionState.DeviceSleep @@ -221,7 +221,7 @@ class MeshConnectionManagerImplTest { every { locationManager.stop() } returns Unit every { mqttManager.stop() } returns Unit - manager.start(backgroundScope) + manager = createManager(backgroundScope) advanceUntilIdle() radioConnectionState.value = ConnectionState.DeviceSleep @@ -236,7 +236,7 @@ class MeshConnectionManagerImplTest { @Test fun `onRadioConfigLoaded enqueues queued packets and sets time`() = runTest(testDispatcher) { - manager.start(backgroundScope) + manager = createManager(backgroundScope) val packetId = 456 everySuspend { packetRepository.getQueuedPackets() } returns listOf(dataPacket) every { workerManager.enqueueSendMessage(any()) } returns Unit @@ -257,15 +257,15 @@ class MeshConnectionManagerImplTest { moduleConfigFlow.value = moduleConfig every { commandSender.requestTelemetry(any(), any(), any()) } returns Unit every { nodeManager.myNodeNum } returns MutableStateFlow(123) - every { mqttManager.start(any(), any(), any()) } returns Unit + every { mqttManager.startProxy(any(), any()) } returns Unit every { historyManager.requestHistoryReplay(any(), any(), any(), any()) } returns Unit every { nodeManager.getMyNodeInfo() } returns null - manager.start(backgroundScope) + manager = createManager(backgroundScope) manager.onNodeDbReady() advanceUntilIdle() - verify { mqttManager.start(any(), true, true) } + verify { mqttManager.startProxy(true, true) } verify { historyManager.requestHistoryReplay(any(), any(), any(), any()) } } @@ -286,7 +286,7 @@ class MeshConnectionManagerImplTest { every { mqttManager.stop() } returns Unit every { nodeManager.nodeDBbyNodeNum } returns emptyMap() - manager.start(backgroundScope) + manager = createManager(backgroundScope) advanceUntilIdle() // Transition to Connected then DeviceSleep diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt index 5f738b439..022608be1 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerTest.kt @@ -108,8 +108,8 @@ class MeshDataHandlerTest { storeForwardHandler = storeForwardHandler, telemetryHandler = telemetryHandler, adminPacketHandler = adminPacketHandler, + scope = testScope, ) - handler.start(testScope) // Default: mapper returns null for empty packets, which is the safe default every { dataMapper.toDataPacket(any()) } returns null diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImplTest.kt index 3090cf49e..251aefabe 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImplTest.kt @@ -23,6 +23,7 @@ import dev.mokkery.matcher.any import dev.mokkery.mock import dev.mokkery.verify import dev.mokkery.verifySuspend +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -65,22 +66,22 @@ class MeshMessageProcessorImplTest { every { nodeManager.isNodeDbReady } returns isNodeDbReady every { nodeManager.myNodeNum } returns MutableStateFlow(myNodeNum) every { router.dataHandler } returns dataHandler - - processor = - MeshMessageProcessorImpl( - nodeManager = nodeManager, - serviceRepository = serviceRepository, - meshLogRepository = lazy { meshLogRepository }, - router = lazy { router }, - fromRadioDispatcher = fromRadioDispatcher, - ) } + private fun createProcessor(scope: CoroutineScope): MeshMessageProcessorImpl = MeshMessageProcessorImpl( + nodeManager = nodeManager, + serviceRepository = serviceRepository, + meshLogRepository = lazy { meshLogRepository }, + router = lazy { router }, + fromRadioDispatcher = fromRadioDispatcher, + scope = scope, + ) + // ---------- handleFromRadio: non-packet variants ---------- @Test fun `handleFromRadio dispatches non-packet variants to fromRadioDispatcher`() = runTest(testDispatcher) { - processor.start(backgroundScope) + processor = createProcessor(backgroundScope) val logRecord = LogRecord(message = "test log") val fromRadio = FromRadio(log_record = logRecord) val bytes = FromRadio.ADAPTER.encode(fromRadio) @@ -93,7 +94,7 @@ class MeshMessageProcessorImplTest { @Test fun `handleFromRadio falls back to LogRecord parsing when FromRadio fails`() = runTest(testDispatcher) { - processor.start(backgroundScope) + processor = createProcessor(backgroundScope) // Encode a raw LogRecord (not wrapped in FromRadio) — first decode as FromRadio fails, // fallback decode as LogRecord succeeds val logRecord = LogRecord(message = "fallback log") @@ -108,7 +109,7 @@ class MeshMessageProcessorImplTest { @Test fun `handleFromRadio with completely invalid bytes does not crash`() = runTest(testDispatcher) { - processor.start(backgroundScope) + processor = createProcessor(backgroundScope) // Invalid protobuf bytes — both parses should fail val garbage = byteArrayOf(0xFF.toByte(), 0xFE.toByte(), 0xFD.toByte()) @@ -121,7 +122,7 @@ class MeshMessageProcessorImplTest { @Test fun `packets are buffered when node DB is not ready`() = runTest(testDispatcher) { - processor.start(backgroundScope) + processor = createProcessor(backgroundScope) isNodeDbReady.value = false val packet = @@ -141,7 +142,7 @@ class MeshMessageProcessorImplTest { @Test fun `buffered packets are flushed when node DB becomes ready`() = runTest(testDispatcher) { - processor.start(backgroundScope) + processor = createProcessor(backgroundScope) isNodeDbReady.value = false val packet = @@ -165,7 +166,7 @@ class MeshMessageProcessorImplTest { @Test fun `early buffer overflow drops oldest packet`() = runTest(testDispatcher) { - processor.start(backgroundScope) + processor = createProcessor(backgroundScope) isNodeDbReady.value = false // The maxEarlyPacketBuffer is 10240 — we won't actually fill it in this test, @@ -195,7 +196,7 @@ class MeshMessageProcessorImplTest { @Test fun `packets with rx_time 0 get current time`() = runTest(testDispatcher) { - processor.start(backgroundScope) + processor = createProcessor(backgroundScope) isNodeDbReady.value = true val packet = @@ -214,7 +215,7 @@ class MeshMessageProcessorImplTest { @Test fun `packets with non-zero rx_time keep their time`() = runTest(testDispatcher) { - processor.start(backgroundScope) + processor = createProcessor(backgroundScope) isNodeDbReady.value = true val packet = @@ -235,7 +236,7 @@ class MeshMessageProcessorImplTest { @Test fun `processReceivedMeshPacket updates myNode lastHeard`() = runTest(testDispatcher) { - processor.start(backgroundScope) + processor = createProcessor(backgroundScope) isNodeDbReady.value = true val packet = @@ -255,7 +256,7 @@ class MeshMessageProcessorImplTest { @Test fun `processReceivedMeshPacket updates sender node`() = runTest(testDispatcher) { - processor.start(backgroundScope) + processor = createProcessor(backgroundScope) isNodeDbReady.value = true val senderNode = 999 @@ -279,7 +280,7 @@ class MeshMessageProcessorImplTest { @Test fun `packet with null decoded is skipped`() = runTest(testDispatcher) { - processor.start(backgroundScope) + processor = createProcessor(backgroundScope) isNodeDbReady.value = true val packet = MeshPacket(id = 1, from = 999, decoded = null) @@ -293,7 +294,7 @@ class MeshMessageProcessorImplTest { @Test fun `processReceivedMeshPacket with null myNodeNum skips node updates`() = runTest(testDispatcher) { - processor.start(backgroundScope) + processor = createProcessor(backgroundScope) isNodeDbReady.value = true val packet = @@ -315,7 +316,7 @@ class MeshMessageProcessorImplTest { @Test fun `clearEarlyPackets empties the buffer`() = runTest(testDispatcher) { - processor.start(backgroundScope) + processor = createProcessor(backgroundScope) isNodeDbReady.value = false val packet = @@ -342,7 +343,7 @@ class MeshMessageProcessorImplTest { @Test fun `FromRadio log_record variant is logged as MeshLog`() = runTest(testDispatcher) { - processor.start(backgroundScope) + processor = createProcessor(backgroundScope) val logRecord = LogRecord(message = "device log") val fromRadio = FromRadio(log_record = logRecord) val bytes = FromRadio.ADAPTER.encode(fromRadio) diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/NodeManagerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/NodeManagerImplTest.kt index 022590467..509066867 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/NodeManagerImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/NodeManagerImplTest.kt @@ -18,6 +18,7 @@ package org.meshtastic.core.data.manager import dev.mokkery.MockMode import dev.mokkery.mock +import kotlinx.coroutines.test.TestScope import okio.ByteString import okio.ByteString.Companion.toByteString import org.meshtastic.core.model.DataPacket @@ -44,12 +45,13 @@ class NodeManagerImplTest { private val nodeRepository: NodeRepository = mock(MockMode.autofill) private val serviceBroadcasts: ServiceBroadcasts = mock(MockMode.autofill) private val notificationManager: NotificationManager = mock(MockMode.autofill) + private val testScope = TestScope() private lateinit var nodeManager: NodeManagerImpl @BeforeTest fun setUp() { - nodeManager = NodeManagerImpl(nodeRepository, serviceBroadcasts, notificationManager) + nodeManager = NodeManagerImpl(nodeRepository, serviceBroadcasts, notificationManager, testScope) } @Test diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt index fe89063ef..0a1698c9a 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt @@ -70,8 +70,8 @@ class PacketHandlerImplTest { radioInterfaceService, lazy { meshLogRepository }, serviceRepository, + testScope, ) - handler.start(testScope) } @Test diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImplTest.kt index e465aaa63..900245332 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/StoreForwardPacketHandlerImplTest.kt @@ -72,8 +72,8 @@ class StoreForwardPacketHandlerImplTest { serviceBroadcasts = serviceBroadcasts, historyManager = historyManager, dataHandler = lazy { dataHandler }, + scope = testScope, ) - handler.start(testScope) } private fun makeSfPacket(from: Int, sf: StoreAndForward): MeshPacket { diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/TelemetryPacketHandlerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/TelemetryPacketHandlerImplTest.kt index 8f295a2b6..28bf22fdc 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/TelemetryPacketHandlerImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/TelemetryPacketHandlerImplTest.kt @@ -62,8 +62,8 @@ class TelemetryPacketHandlerImplTest { nodeManager = nodeManager, connectionManager = lazy { connectionManager }, notificationManager = notificationManager, + scope = testScope, ) - handler.start(testScope) } private fun makeTelemetryPacket(from: Int, telemetry: Telemetry): MeshPacket { diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/CommandSender.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/CommandSender.kt index 2b897baa9..b99a002de 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/CommandSender.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/CommandSender.kt @@ -16,7 +16,6 @@ */ package org.meshtastic.core.repository -import kotlinx.coroutines.CoroutineScope import okio.ByteString import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.Position @@ -27,9 +26,6 @@ import org.meshtastic.proto.LocalConfig /** Interface for sending commands and packets to the mesh network. */ @Suppress("TooManyFunctions") interface CommandSender { - /** Starts the command sender with the given coroutine scope. */ - fun start(scope: CoroutineScope) - /** Returns the current packet ID. */ fun getCurrentPacketId(): Long diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshActionHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshActionHandler.kt index ac92e8287..5c43efdcd 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshActionHandler.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshActionHandler.kt @@ -16,7 +16,6 @@ */ package org.meshtastic.core.repository -import kotlinx.coroutines.CoroutineScope import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MeshUser import org.meshtastic.core.model.Position @@ -25,9 +24,6 @@ import org.meshtastic.core.model.service.ServiceAction /** Interface for handling UI-triggered actions and administrative commands for the mesh. */ @Suppress("TooManyFunctions") interface MeshActionHandler { - /** Starts the handler with the given coroutine scope. */ - fun start(scope: CoroutineScope) - /** Processes a service action from the UI. */ suspend fun onServiceAction(action: ServiceAction) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConfigFlowManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConfigFlowManager.kt index 2a92f8909..b2bb6d418 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConfigFlowManager.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConfigFlowManager.kt @@ -16,7 +16,6 @@ */ package org.meshtastic.core.repository -import kotlinx.coroutines.CoroutineScope import org.meshtastic.proto.DeviceMetadata import org.meshtastic.proto.FileInfo import org.meshtastic.proto.MyNodeInfo @@ -24,9 +23,6 @@ import org.meshtastic.proto.NodeInfo /** Interface for managing the configuration flow, including local node info and metadata. */ interface MeshConfigFlowManager { - /** Starts the manager with the given coroutine scope. */ - fun start(scope: CoroutineScope) - /** Handles received local node information. */ fun handleMyInfo(myInfo: MyNodeInfo) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConfigHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConfigHandler.kt index 3f3887631..c0e60337e 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConfigHandler.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConfigHandler.kt @@ -16,7 +16,6 @@ */ package org.meshtastic.core.repository -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow import org.meshtastic.proto.Channel import org.meshtastic.proto.Config @@ -27,9 +26,6 @@ import org.meshtastic.proto.ModuleConfig /** Interface for handling device and module configuration updates. */ interface MeshConfigHandler { - /** Starts the handler with the given coroutine scope. */ - fun start(scope: CoroutineScope) - /** Reactive local configuration. */ val localConfig: StateFlow diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConnectionManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConnectionManager.kt index eae5bd9a0..c60db9afa 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConnectionManager.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshConnectionManager.kt @@ -16,14 +16,10 @@ */ package org.meshtastic.core.repository -import kotlinx.coroutines.CoroutineScope import org.meshtastic.proto.Telemetry /** Interface for managing the connection lifecycle and status with the mesh radio. */ interface MeshConnectionManager { - /** Starts the connection manager with the given coroutine scope. */ - fun start(scope: CoroutineScope) - /** Called when the radio configuration has been fully loaded. */ fun onRadioConfigLoaded() diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshDataHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshDataHandler.kt index 2c7487cf9..7d5f2a913 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshDataHandler.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshDataHandler.kt @@ -16,16 +16,12 @@ */ package org.meshtastic.core.repository -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import org.meshtastic.core.model.DataPacket import org.meshtastic.proto.MeshPacket /** Interface for handling incoming mesh data packets and routing them to the appropriate handlers. */ interface MeshDataHandler { - /** Starts the handler with the given coroutine scope. */ - fun start(scope: CoroutineScope) - /** * Processes a received mesh packet. * diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshMessageProcessor.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshMessageProcessor.kt index 1a3657d9e..a8d6545ce 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshMessageProcessor.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshMessageProcessor.kt @@ -16,14 +16,10 @@ */ package org.meshtastic.core.repository -import kotlinx.coroutines.CoroutineScope import org.meshtastic.proto.MeshPacket /** Interface for processing incoming radio messages and mesh packets. */ interface MeshMessageProcessor { - /** Starts the processor with the given coroutine scope. */ - fun start(scope: CoroutineScope) - /** Handles a raw message received from the radio. */ fun handleFromRadio(bytes: ByteArray, myNodeNum: Int?) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshRouter.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshRouter.kt index be2830af9..42b306b17 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshRouter.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshRouter.kt @@ -16,13 +16,8 @@ */ package org.meshtastic.core.repository -import kotlinx.coroutines.CoroutineScope - /** Interface for the central router that orchestrates specialized mesh packet handlers. */ interface MeshRouter { - /** Starts the router and its sub-components with the given coroutine scope. */ - fun start(scope: CoroutineScope) - /** Access to the data handler. */ val dataHandler: MeshDataHandler diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MqttManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MqttManager.kt index cfda5a9d0..7ebfa0521 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MqttManager.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MqttManager.kt @@ -16,13 +16,12 @@ */ package org.meshtastic.core.repository -import kotlinx.coroutines.CoroutineScope import org.meshtastic.proto.MqttClientProxyMessage /** Interface for managing MQTT proxy communication. */ interface MqttManager { - /** Starts the MQTT manager with the given coroutine scope and settings. */ - fun start(scope: CoroutineScope, enabled: Boolean, proxyToClientEnabled: Boolean) + /** Starts the MQTT proxy with the given settings. */ + fun startProxy(enabled: Boolean, proxyToClientEnabled: Boolean) /** Stops the MQTT manager. */ fun stop() diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NeighborInfoHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NeighborInfoHandler.kt index b9759ff59..903146331 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NeighborInfoHandler.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NeighborInfoHandler.kt @@ -16,15 +16,11 @@ */ package org.meshtastic.core.repository -import kotlinx.coroutines.CoroutineScope import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.NeighborInfo /** Interface for handling neighbor info responses from the mesh. */ interface NeighborInfoHandler { - /** Starts the neighbor info handler with the given coroutine scope. */ - fun start(scope: CoroutineScope) - /** Records the start time for a neighbor info request. */ fun recordStartTime(requestId: Int) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeManager.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeManager.kt index a0d115391..ac6718572 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeManager.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeManager.kt @@ -16,7 +16,6 @@ */ package org.meshtastic.core.repository -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.model.Node @@ -51,9 +50,6 @@ interface NodeManager : NodeIdLookup { /** Sets whether node database writes are allowed. */ fun setAllowNodeDbWrites(allowed: Boolean) - /** Starts the node manager with the given coroutine scope. */ - fun start(scope: CoroutineScope) - /** The local node number as a thread-safe [StateFlow]. */ val myNodeNum: StateFlow diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketHandler.kt index 686840f40..081e2928b 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketHandler.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/PacketHandler.kt @@ -16,16 +16,12 @@ */ package org.meshtastic.core.repository -import kotlinx.coroutines.CoroutineScope import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.QueueStatus import org.meshtastic.proto.ToRadio /** Interface for handling the transmission of packets to the radio and managing the packet queue. */ interface PacketHandler { - /** Starts the packet handler with the given coroutine scope. */ - fun start(scope: CoroutineScope) - /** Sends a command/packet directly to the radio. */ fun sendToRadio(p: ToRadio) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/StoreForwardPacketHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/StoreForwardPacketHandler.kt index 51006763d..bda122ac1 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/StoreForwardPacketHandler.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/StoreForwardPacketHandler.kt @@ -16,15 +16,11 @@ */ package org.meshtastic.core.repository -import kotlinx.coroutines.CoroutineScope import org.meshtastic.core.model.DataPacket import org.meshtastic.proto.MeshPacket /** Interface for handling Store & Forward (legacy) and SF++ packets. */ interface StoreForwardPacketHandler { - /** Starts the handler with the given coroutine scope. */ - fun start(scope: CoroutineScope) - /** * Handles a legacy Store & Forward packet. * diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/TelemetryPacketHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/TelemetryPacketHandler.kt index a53cd8b8a..b1f1aa2c9 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/TelemetryPacketHandler.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/TelemetryPacketHandler.kt @@ -16,15 +16,11 @@ */ package org.meshtastic.core.repository -import kotlinx.coroutines.CoroutineScope import org.meshtastic.core.model.DataPacket import org.meshtastic.proto.MeshPacket /** Interface for handling telemetry packets from the mesh, including battery notifications. */ interface TelemetryPacketHandler { - /** Starts the handler with the given coroutine scope. */ - fun start(scope: CoroutineScope) - /** * Processes a telemetry packet. * diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/TracerouteHandler.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/TracerouteHandler.kt index aa2e6318a..6535ef30c 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/TracerouteHandler.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/TracerouteHandler.kt @@ -16,15 +16,11 @@ */ package org.meshtastic.core.repository -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import org.meshtastic.proto.MeshPacket /** Interface for handling traceroute responses from the mesh. */ interface TracerouteHandler { - /** Starts the traceroute handler with the given coroutine scope. */ - fun start(scope: CoroutineScope) - /** Records the start time for a traceroute request. */ fun recordStartTime(requestId: Int) 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 7e9832b54..e651d95ce 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,16 +22,14 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import org.koin.core.annotation.Named import org.koin.core.annotation.Single import org.meshtastic.core.common.database.DatabaseManager 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.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 org.meshtastic.core.repository.TakPrefs @@ -51,25 +49,22 @@ import org.meshtastic.core.takserver.TAKServerManager class MeshServiceOrchestrator( private val radioInterfaceService: RadioInterfaceService, private val serviceRepository: ServiceRepository, - private val packetHandler: PacketHandler, private val nodeManager: NodeManager, private val messageProcessor: MeshMessageProcessor, - private val commandSender: CommandSender, - private val connectionManager: MeshConnectionManager, private val router: MeshRouter, private val serviceNotifications: MeshServiceNotifications, private val takServerManager: TAKServerManager, private val takMeshIntegration: TAKMeshIntegration, private val takPrefs: TakPrefs, - private val dispatchers: org.meshtastic.core.di.CoroutineDispatchers, private val databaseManager: DatabaseManager, + @Named("ServiceScope") private val scope: CoroutineScope, ) { private var serviceJob: Job? = null private var takJob: Job? = null - /** The coroutine scope for the service. Available after [start] is called. */ - var serviceScope: CoroutineScope? = null - private set + /** The coroutine scope for the service. */ + val serviceScope: CoroutineScope + get() = scope /** Whether the orchestrator is currently running. */ val isRunning: Boolean @@ -78,8 +73,8 @@ class MeshServiceOrchestrator( /** * Starts the mesh service components and wires up data flows. * - * This is the KMP equivalent of `MeshService.onCreate()`. It starts all managers, connects to the radio, and wires - * incoming radio data to the message processor and service actions to the router's action handler. + * This is the KMP equivalent of `MeshService.onCreate()`. It connects to the radio and wires incoming radio data to + * the message processor and service actions to the router's action handler. */ fun start() { if (isRunning) { @@ -90,18 +85,9 @@ class MeshServiceOrchestrator( Logger.i { "Starting mesh service orchestrator" } val job = Job() serviceJob = job - val scope = CoroutineScope(dispatchers.default + job) - serviceScope = scope serviceNotifications.initChannels() - packetHandler.start(scope) - router.start(scope) - nodeManager.start(scope) - connectionManager.start(scope) - messageProcessor.start(scope) - commandSender.start(scope) - // Observe TAK server pref to start/stop takJob = takPrefs.isTakServerEnabled @@ -161,6 +147,5 @@ class MeshServiceOrchestrator( } serviceJob?.cancel() serviceJob = null - serviceScope = null } } diff --git a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/di/CoreServiceModule.kt b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/di/CoreServiceModule.kt index d007f1ea3..3fae4287b 100644 --- a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/di/CoreServiceModule.kt +++ b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/di/CoreServiceModule.kt @@ -16,9 +16,19 @@ */ package org.meshtastic.core.service.di +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob import org.koin.core.annotation.ComponentScan import org.koin.core.annotation.Module +import org.koin.core.annotation.Named +import org.koin.core.annotation.Single +import org.meshtastic.core.di.CoroutineDispatchers @Module @ComponentScan("org.meshtastic.core.service") -class CoreServiceModule +class CoreServiceModule { + @Single + @Named("ServiceScope") + fun provideServiceScope(dispatchers: CoroutineDispatchers): CoroutineScope = + CoroutineScope(dispatchers.default + SupervisorJob()) +} 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 611454d05..48be7dbf6 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 @@ -25,23 +25,21 @@ import dev.mokkery.mock import dev.mokkery.verify import dev.mokkery.verify.VerifyMode.Companion.exactly import dev.mokkery.verifySuspend +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.UnconfinedTestDispatcher import org.meshtastic.core.common.database.DatabaseManager -import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.Node import org.meshtastic.core.model.service.ServiceAction import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.MeshActionHandler import org.meshtastic.core.repository.MeshConfigHandler -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.NodeRepository -import org.meshtastic.core.repository.PacketHandler import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.repository.TakPrefs @@ -57,12 +55,10 @@ class MeshServiceOrchestratorTest { private val radioInterfaceService: RadioInterfaceService = mock(MockMode.autofill) private val serviceRepository: ServiceRepository = mock(MockMode.autofill) - private val packetHandler: PacketHandler = mock(MockMode.autofill) private val nodeManager: NodeManager = mock(MockMode.autofill) private val nodeRepository: NodeRepository = mock(MockMode.autofill) private val messageProcessor: MeshMessageProcessor = mock(MockMode.autofill) private val commandSender: CommandSender = mock(MockMode.autofill) - private val connectionManager: MeshConnectionManager = mock(MockMode.autofill) private val router: MeshRouter = mock(MockMode.autofill) private val actionHandler: MeshActionHandler = mock(MockMode.autofill) private val meshConfigHandler: MeshConfigHandler = mock(MockMode.autofill) @@ -73,7 +69,7 @@ class MeshServiceOrchestratorTest { private val databaseManager: DatabaseManager = mock(MockMode.autofill) private val testDispatcher = UnconfinedTestDispatcher() - private val dispatchers = CoroutineDispatchers(testDispatcher, testDispatcher, testDispatcher) + private val testScope = CoroutineScope(testDispatcher) /** Stubs the shared flow dependencies used by every test and returns an orchestrator. */ private fun createOrchestrator( @@ -107,18 +103,15 @@ class MeshServiceOrchestratorTest { return MeshServiceOrchestrator( radioInterfaceService = radioInterfaceService, serviceRepository = serviceRepository, - packetHandler = packetHandler, nodeManager = nodeManager, messageProcessor = messageProcessor, - commandSender = commandSender, - connectionManager = connectionManager, router = router, serviceNotifications = serviceNotifications, takServerManager = takServerManager, takMeshIntegration = takMeshIntegration, takPrefs = takPrefs, - dispatchers = dispatchers, databaseManager = databaseManager, + scope = testScope, ) } @@ -131,7 +124,6 @@ class MeshServiceOrchestratorTest { assertTrue(orchestrator.isRunning) verify { serviceNotifications.initChannels() } - verify { packetHandler.start(any()) } verify { nodeManager.loadCachedNodeDB() } orchestrator.stop() @@ -217,7 +209,6 @@ class MeshServiceOrchestratorTest { // Components should only be initialized once verify(exactly(1)) { serviceNotifications.initChannels() } - verify(exactly(1)) { packetHandler.start(any()) } verify(exactly(1)) { nodeManager.loadCachedNodeDB() } orchestrator.stop()