refactor(transport): complete transport architecture overhaul — extract callback, wire BleReconnectPolicy, fix safety issues (#5080)

This commit is contained in:
James Rich 2026-04-11 23:22:18 -05:00 committed by GitHub
parent 962c619c4c
commit e85300531e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
64 changed files with 1184 additions and 1018 deletions

View file

@ -34,16 +34,25 @@ import org.meshtastic.core.model.NetworkFirmwareReleases
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.network.KermitHttpLogger
import org.meshtastic.core.network.repository.MQTTRepository
import org.meshtastic.core.network.service.ApiService
import org.meshtastic.core.network.service.ApiServiceImpl
import org.meshtastic.core.repository.AppWidgetUpdater
import org.meshtastic.core.repository.LocationRepository
import org.meshtastic.core.repository.MeshLocationManager
import org.meshtastic.core.repository.MeshServiceNotifications
import org.meshtastic.core.repository.MeshWorkerManager
import org.meshtastic.core.repository.MessageQueue
import org.meshtastic.core.repository.NotificationManager
import org.meshtastic.core.repository.PlatformAnalytics
import org.meshtastic.core.repository.RadioTransportFactory
import org.meshtastic.core.repository.ServiceBroadcasts
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.service.DirectRadioControllerImpl
import org.meshtastic.core.service.ServiceRepositoryImpl
import org.meshtastic.desktop.DesktopBuildConfig
import org.meshtastic.desktop.DesktopNotificationManager
import org.meshtastic.desktop.notification.DesktopMeshServiceNotifications
import org.meshtastic.desktop.radio.DesktopMessageQueue
import org.meshtastic.desktop.radio.DesktopRadioTransportFactory
import org.meshtastic.desktop.stub.NoopAppWidgetUpdater
import org.meshtastic.desktop.stub.NoopCompassHeadingProvider
@ -55,6 +64,9 @@ import org.meshtastic.desktop.stub.NoopMeshWorkerManager
import org.meshtastic.desktop.stub.NoopPhoneLocationProvider
import org.meshtastic.desktop.stub.NoopPlatformAnalytics
import org.meshtastic.desktop.stub.NoopServiceBroadcasts
import org.meshtastic.feature.node.compass.CompassHeadingProvider
import org.meshtastic.feature.node.compass.MagneticFieldProvider
import org.meshtastic.feature.node.compass.PhoneLocationProvider
import org.meshtastic.core.ble.di.module as coreBleModule
import org.meshtastic.core.common.di.module as coreCommonModule
import org.meshtastic.core.data.di.module as coreDataModule
@ -124,7 +136,7 @@ fun desktopModule() = module {
*/
@Suppress("LongMethod")
private fun desktopPlatformStubsModule() = module {
single<ServiceRepository> { org.meshtastic.core.service.ServiceRepositoryImpl() }
single<ServiceRepository> { ServiceRepositoryImpl() }
single<RadioTransportFactory> {
DesktopRadioTransportFactory(
dispatchers = get(),
@ -134,7 +146,7 @@ private fun desktopPlatformStubsModule() = module {
)
}
single<RadioController> {
org.meshtastic.core.service.DirectRadioControllerImpl(
DirectRadioControllerImpl(
serviceRepository = get(),
nodeRepository = get(),
commandSender = get(),
@ -144,37 +156,29 @@ private fun desktopPlatformStubsModule() = module {
locationManager = get(),
)
}
single { org.meshtastic.desktop.DesktopNotificationManager(prefs = get()) }
single<org.meshtastic.core.repository.NotificationManager> {
get<org.meshtastic.desktop.DesktopNotificationManager>()
}
single<MeshServiceNotifications> {
org.meshtastic.desktop.notification.DesktopMeshServiceNotifications(notificationManager = get())
}
single { DesktopNotificationManager(prefs = get()) }
single<NotificationManager> { get<DesktopNotificationManager>() }
single<MeshServiceNotifications> { DesktopMeshServiceNotifications(notificationManager = get()) }
single<PlatformAnalytics> { NoopPlatformAnalytics() }
single<ServiceBroadcasts> { NoopServiceBroadcasts() }
single<AppWidgetUpdater> { NoopAppWidgetUpdater() }
single<MeshWorkerManager> { NoopMeshWorkerManager() }
single<MessageQueue> {
org.meshtastic.desktop.radio.DesktopMessageQueue(packetRepository = get(), radioController = get())
}
single<MessageQueue> { DesktopMessageQueue(packetRepository = get(), radioController = get()) }
single<MeshLocationManager> { NoopMeshLocationManager() }
single<LocationRepository> { NoopLocationRepository() }
single<MQTTRepository> { NoopMQTTRepository() }
single<org.meshtastic.feature.node.compass.CompassHeadingProvider> { NoopCompassHeadingProvider() }
single<org.meshtastic.feature.node.compass.PhoneLocationProvider> { NoopPhoneLocationProvider() }
single<org.meshtastic.feature.node.compass.MagneticFieldProvider> { NoopMagneticFieldProvider() }
single<CompassHeadingProvider> { NoopCompassHeadingProvider() }
single<PhoneLocationProvider> { NoopPhoneLocationProvider() }
single<MagneticFieldProvider> { NoopMagneticFieldProvider() }
// Desktop uses the real ApiService implementation (no flavor stub needed)
single<org.meshtastic.core.network.service.ApiService> {
org.meshtastic.core.network.service.ApiServiceImpl(client = get())
}
single<ApiService> { ApiServiceImpl(client = get()) }
// Ktor HttpClient for JVM/Desktop (equivalent of CoreNetworkAndroidModule on Android)
single<HttpClient> {
HttpClient(Java) {
install(ContentNegotiation) { json(get<Json>()) }
if (org.meshtastic.desktop.DesktopBuildConfig.IS_DEBUG) {
if (DesktopBuildConfig.IS_DEBUG) {
install(Logging) {
logger = KermitHttpLogger
level = LogLevel.HEADERS

View file

@ -24,7 +24,7 @@ import org.meshtastic.core.model.DeviceType
import org.meshtastic.core.model.InterfaceId
import org.meshtastic.core.network.SerialTransport
import org.meshtastic.core.network.radio.BaseRadioTransportFactory
import org.meshtastic.core.network.radio.TCPInterface
import org.meshtastic.core.network.radio.TcpRadioTransport
import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.core.repository.RadioTransport
import org.meshtastic.core.repository.RadioTransportFactory
@ -45,16 +45,22 @@ class DesktopRadioTransportFactory(
override val supportedDeviceTypes: List<DeviceType> = listOf(DeviceType.TCP, DeviceType.BLE, DeviceType.USB)
override fun isMockInterface(): Boolean = false
override fun isMockTransport(): Boolean = false
override fun createPlatformTransport(address: String, service: RadioInterfaceService): RadioTransport = when {
address.startsWith(InterfaceId.TCP.id) -> {
TCPInterface(service, dispatchers, address.removePrefix(InterfaceId.TCP.id.toString()))
TcpRadioTransport(
callback = service,
scope = service.serviceScope,
dispatchers = dispatchers,
address = address.removePrefix(InterfaceId.TCP.id.toString()),
)
}
address.startsWith(InterfaceId.SERIAL.id) -> {
SerialTransport.open(
portName = address.removePrefix(InterfaceId.SERIAL.id.toString()),
service = service,
callback = service,
scope = service.serviceScope,
dispatchers = dispatchers,
)
}

View file

@ -20,6 +20,7 @@ package org.meshtastic.desktop.stub
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
@ -27,6 +28,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.DeviceType
import org.meshtastic.core.model.InterfaceId
import org.meshtastic.core.model.MeshActivity
import org.meshtastic.core.model.MessageStatus
@ -37,14 +39,11 @@ import org.meshtastic.core.repository.DataPair
import org.meshtastic.core.repository.Location
import org.meshtastic.core.repository.LocationRepository
import org.meshtastic.core.repository.MeshLocationManager
import org.meshtastic.core.repository.MeshServiceNotifications
import org.meshtastic.core.repository.MeshWorkerManager
import org.meshtastic.core.repository.PlatformAnalytics
import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.core.repository.ServiceBroadcasts
import org.meshtastic.proto.ClientNotification
import org.meshtastic.proto.MqttClientProxyMessage
import org.meshtastic.proto.Telemetry
import org.meshtastic.proto.Position as ProtoPosition
/**
@ -66,12 +65,12 @@ private fun logWarn(message: String) {
// region Transport / Radio Stubs (Android BLE/USB — no commonMain impl)
class NoopRadioInterfaceService : RadioInterfaceService {
override val supportedDeviceTypes: List<org.meshtastic.core.model.DeviceType> = emptyList()
override val supportedDeviceTypes: List<DeviceType> = emptyList()
override val connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected)
override val currentDeviceAddressFlow = MutableStateFlow<String?>(null)
override fun isMockInterface(): Boolean = false
override fun isMockTransport(): Boolean = false
override val receivedData = MutableSharedFlow<ByteArray>()
override val meshActivity = MutableSharedFlow<MeshActivity>()
@ -98,65 +97,13 @@ class NoopRadioInterfaceService : RadioInterfaceService {
override fun handleFromRadio(bytes: ByteArray) {}
@Suppress("InjectDispatcher")
override val serviceScope: CoroutineScope = CoroutineScope(SupervisorJob() + kotlinx.coroutines.Dispatchers.Default)
override val serviceScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
}
// endregion
// region Notification / Platform Stubs (Android-only)
@Suppress("TooManyFunctions")
class NoopMeshServiceNotifications : MeshServiceNotifications {
override fun clearNotifications() {}
override fun initChannels() {}
override fun updateServiceStateNotification(
state: org.meshtastic.core.model.ConnectionState,
telemetry: Telemetry?,
) {}
override suspend fun updateMessageNotification(
contactKey: String,
name: String,
message: String,
isBroadcast: Boolean,
channelName: String?,
isSilent: Boolean,
) {}
override suspend fun updateWaypointNotification(
contactKey: String,
name: String,
message: String,
waypointId: Int,
isSilent: Boolean,
) {}
override suspend fun updateReactionNotification(
contactKey: String,
name: String,
emoji: String,
isBroadcast: Boolean,
channelName: String?,
isSilent: Boolean,
) {}
override fun showAlertNotification(contactKey: String, name: String, alert: String) {}
override fun showNewNodeSeenNotification(node: Node) {}
override fun showOrUpdateLowBatteryNotification(node: Node, isRemote: Boolean) {}
override fun showClientNotification(clientNotification: ClientNotification) {}
override fun cancelMessageNotification(contactKey: String) {}
override fun cancelLowBatteryNotification(node: Node) {}
override fun clearClientNotification(notification: ClientNotification) {}
}
class NoopPlatformAnalytics : PlatformAnalytics {
override fun track(event: String, vararg properties: DataPair) {}