feat: service extraction (#4828)

This commit is contained in:
James Rich 2026-03-17 14:06:01 -05:00 committed by GitHub
parent 0d0bdf9172
commit 807db83f53
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
76 changed files with 309 additions and 257 deletions

View file

@ -49,11 +49,11 @@ import org.koin.core.context.startKoin
import org.meshtastic.core.datastore.UiPreferencesDataSource
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.navigation.TopLevelDestination
import org.meshtastic.core.service.MeshServiceOrchestrator
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.desktop.data.DesktopPreferencesDataSource
import org.meshtastic.desktop.di.desktopModule
import org.meshtastic.desktop.di.desktopPlatformModule
import org.meshtastic.desktop.radio.DesktopMeshServiceController
import org.meshtastic.desktop.ui.DesktopMainScreen
import org.meshtastic.desktop.ui.navSavedStateConfig
import java.util.Locale
@ -82,7 +82,7 @@ fun main() = application(exitProcessOnExit = false) {
val systemLocale = remember { Locale.getDefault() }
// Start the mesh service processing chain (desktop equivalent of Android's MeshService)
val meshServiceController = remember { koinApp.koin.get<DesktopMeshServiceController>() }
val meshServiceController = remember { koinApp.koin.get<MeshServiceOrchestrator>() }
DisposableEffect(Unit) {
meshServiceController.start()
onDispose { meshServiceController.stop() }

View file

@ -41,7 +41,6 @@ import org.meshtastic.core.repository.PlatformAnalytics
import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.core.repository.ServiceBroadcasts
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.desktop.radio.DesktopMeshServiceController
import org.meshtastic.desktop.radio.DesktopRadioInterfaceService
import org.meshtastic.desktop.stub.NoopAppWidgetUpdater
import org.meshtastic.desktop.stub.NoopCompassHeadingProvider
@ -151,19 +150,6 @@ private fun desktopPlatformStubsModule() = module {
single<org.meshtastic.feature.node.compass.MagneticFieldProvider> { NoopMagneticFieldProvider() }
// Desktop mesh service controller — replaces Android's MeshService lifecycle
single {
DesktopMeshServiceController(
radioInterfaceService = get(),
serviceRepository = get(),
messageProcessor = get(),
connectionManager = get(),
packetHandler = get(),
router = get(),
nodeManager = get(),
commandSender = get(),
)
}
// Ktor HttpClient for JVM/Desktop (equivalent of CoreNetworkAndroidModule on Android)
single<HttpClient> { HttpClient(Java) { install(ContentNegotiation) { json(get<Json>()) } } }

View file

@ -1,110 +0,0 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.desktop.radio
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.repository.CommandSender
import org.meshtastic.core.repository.MeshConnectionManager
import org.meshtastic.core.repository.MeshMessageProcessor
import org.meshtastic.core.repository.MeshRouter
import org.meshtastic.core.repository.NodeManager
import org.meshtastic.core.repository.PacketHandler
import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.core.repository.ServiceRepository
/**
* Desktop equivalent of Android's `MeshService.onCreate()`.
*
* Starts the full message-processing chain that connects the radio transport layer to the business logic:
* ```
* radioInterfaceService.receivedData
* messageProcessor.handleFromRadio(bytes, myNodeNum)
* FromRadioPacketHandler MeshRouter/PacketHandler/etc.
* ```
*
* On Android this chain runs inside an Android `Service` (foreground service with notifications). On Desktop there is
* no Android Service concept, so this controller manages the same lifecycle in-process, started at app launch time.
*/
@Suppress("LongParameterList")
class DesktopMeshServiceController(
private val radioInterfaceService: RadioInterfaceService,
private val serviceRepository: ServiceRepository,
private val messageProcessor: MeshMessageProcessor,
private val connectionManager: MeshConnectionManager,
private val packetHandler: PacketHandler,
private val router: MeshRouter,
private val nodeManager: NodeManager,
private val commandSender: CommandSender,
) {
private var serviceScope: CoroutineScope? = null
/**
* Starts the mesh service processing chain.
*
* This should be called once at application startup (after Koin is initialized). It mirrors the initialization
* logic from `MeshService.onCreate()`.
*/
@Suppress("InjectDispatcher")
fun start() {
if (serviceScope != null) {
Logger.w { "DesktopMeshServiceController: Already started, ignoring duplicate start()" }
return
}
Logger.i { "DesktopMeshServiceController: Starting mesh service processing chain" }
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
serviceScope = scope
// Start all processing components (same order as MeshService.onCreate)
packetHandler.start(scope)
router.start(scope)
nodeManager.start(scope)
connectionManager.start(scope)
messageProcessor.start(scope)
commandSender.start(scope)
// Auto-connect to saved device address (mirrors MeshService.onCreate)
scope.handledLaunch { radioInterfaceService.connect() }
// Wire the data flow: radio → message processor
radioInterfaceService.receivedData
.onEach { bytes -> messageProcessor.handleFromRadio(bytes, nodeManager.myNodeNum) }
.launchIn(scope)
// Wire service actions to the router
serviceRepository.serviceAction.onEach(router.actionHandler::onServiceAction).launchIn(scope)
// Load any cached node database
nodeManager.loadCachedNodeDB()
Logger.i { "DesktopMeshServiceController: Processing chain started" }
}
/** Stops the mesh service processing chain and cancels all coroutines. */
fun stop() {
Logger.i { "DesktopMeshServiceController: Stopping" }
serviceScope?.cancel("DesktopMeshServiceController stopped")
serviceScope = null
}
}