refactor(data): replace lateinit var scope + start() with constructor injection (#5075)

This commit is contained in:
James Rich 2026-04-11 18:39:29 -05:00 committed by GitHub
parent 172680fd46
commit 174315b21f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 188 additions and 301 deletions

View file

@ -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)
}

View file

@ -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<MeshMessageProcessor>,
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

View file

@ -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.

View file

@ -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)
}

View file

@ -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,
)

View file

@ -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(

View file

@ -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<MeshLogRepository>,
private val router: Lazy<MeshRouter>,
private val fromRadioDispatcher: FromRadioPacketHandler,
@Named("ServiceScope") private val scope: CoroutineScope,
) : MeshMessageProcessor {
private lateinit var scope: CoroutineScope
private val mapsMutex = Mutex()
private val logUuidByPacketId = mutableMapOf<Int, String>()
@ -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) {

View file

@ -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)
}
}

View file

@ -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 =

View file

@ -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<Int, Long>())
override var lastNeighborInfo: NeighborInfo? = null
override fun start(scope: CoroutineScope) {
this.scope = scope
}
override fun recordStartTime(requestId: Int) {
startTimes.update { it.put(requestId, nowMillis) }
}

View file

@ -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<Int, Node>())
private val _nodeDBbyID = atomic(persistentMapOf<String, Node>())
@ -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
}

View file

@ -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<MeshLogRepository>,
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<MeshPacket>()
@ -79,11 +80,6 @@ class PacketHandlerImpl(
private val responseMutex = Mutex()
private val queueResponse = mutableMapOf<Int, CompletableDeferred<Boolean>>()
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()

View file

@ -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<MeshDataHandler>,
@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

View file

@ -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<MeshConnectionManager>,
private val notificationManager: NotificationManager,
@Named("ServiceScope") private val scope: CoroutineScope,
) : TelemetryPacketHandler {
private lateinit var scope: CoroutineScope
private val batteryMutex = Mutex()
private val batteryPercentCooldowns = mutableMapOf<Int, Long>()
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

View file

@ -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<Int, Long>())
override fun start(scope: CoroutineScope) {
this.scope = scope
}
override fun recordStartTime(requestId: Int) {
startTimes.update { it.put(requestId, nowMillis) }
}

View file

@ -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)

View file

@ -98,8 +98,8 @@ class MeshConfigFlowManagerImplTest {
analytics = analytics,
commandSender = commandSender,
packetHandler = packetHandler,
scope = testScope,
)
manager.start(testScope)
}
// ---------- handleMyInfo ----------

View file

@ -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<Int?>(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<Int?>(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()

View file

@ -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<RadioInterfaceService>(MockMode.autofill)
private val serviceRepository = mock<ServiceRepository>(MockMode.autofill)
@ -108,29 +108,29 @@ class MeshConnectionManagerImplTest {
every { mqttManager.stop() } returns Unit
every { nodeManager.nodeDBbyNodeNum } returns emptyMap<Int, Node>()
every { packetHandler.sendToRadio(any<org.meshtastic.proto.ToRadio>()) } 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<org.meshtastic.proto.ToRadio>()) } 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

View file

@ -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

View file

@ -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<Int?>(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)

View file

@ -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

View file

@ -70,8 +70,8 @@ class PacketHandlerImplTest {
radioInterfaceService,
lazy { meshLogRepository },
serviceRepository,
testScope,
)
handler.start(testScope)
}
@Test

View file

@ -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 {

View file

@ -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 {

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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<LocalConfig>

View file

@ -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()

View file

@ -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.
*

View file

@ -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?)

View file

@ -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

View file

@ -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()

View file

@ -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)

View file

@ -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<Int?>

View file

@ -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)

View file

@ -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.
*

View file

@ -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.
*

View file

@ -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)

View file

@ -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
}
}

View file

@ -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())
}

View file

@ -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()