feat: introduce Desktop target and expand Kotlin Multiplatform (KMP) architecture (#4761)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-03-12 16:14:49 -05:00 committed by GitHub
parent f4364cff9a
commit ac6bb5479b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
386 changed files with 17089 additions and 4590 deletions

View file

@ -18,7 +18,7 @@ package org.meshtastic.core.data.datasource
import kotlinx.coroutines.withContext
import org.koin.core.annotation.Single
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.database.DatabaseProvider
import org.meshtastic.core.database.entity.DeviceHardwareEntity
import org.meshtastic.core.database.entity.asEntity
import org.meshtastic.core.di.CoroutineDispatchers
@ -26,7 +26,7 @@ import org.meshtastic.core.model.NetworkDeviceHardware
@Single
class DeviceHardwareLocalDataSource(
private val dbManager: DatabaseManager,
private val dbManager: DatabaseProvider,
private val dispatchers: CoroutineDispatchers,
) {
private val deviceHardwareDao

View file

@ -18,7 +18,7 @@ package org.meshtastic.core.data.datasource
import kotlinx.coroutines.withContext
import org.koin.core.annotation.Single
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.database.DatabaseProvider
import org.meshtastic.core.database.entity.FirmwareReleaseEntity
import org.meshtastic.core.database.entity.FirmwareReleaseType
import org.meshtastic.core.database.entity.asDeviceVersion
@ -28,7 +28,7 @@ import org.meshtastic.core.model.NetworkFirmwareRelease
@Single
class FirmwareReleaseLocalDataSource(
private val dbManager: DatabaseManager,
private val dbManager: DatabaseProvider,
private val dispatchers: CoroutineDispatchers,
) {
private val firmwareReleaseDao

View file

@ -19,13 +19,13 @@ package org.meshtastic.core.data.datasource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapLatest
import org.koin.core.annotation.Single
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.database.DatabaseProvider
import org.meshtastic.core.database.entity.MyNodeEntity
import org.meshtastic.core.database.entity.NodeEntity
import org.meshtastic.core.database.entity.NodeWithRelations
@Single
class SwitchingNodeInfoReadDataSource(private val dbManager: DatabaseManager) : NodeInfoReadDataSource {
class SwitchingNodeInfoReadDataSource(private val dbManager: DatabaseProvider) : NodeInfoReadDataSource {
override fun myNodeInfoFlow(): Flow<MyNodeEntity?> =
dbManager.currentDb.flatMapLatest { db -> db.nodeInfoDao().getMyNodeInfo() }

View file

@ -18,7 +18,7 @@ package org.meshtastic.core.data.datasource
import kotlinx.coroutines.withContext
import org.koin.core.annotation.Single
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.database.DatabaseProvider
import org.meshtastic.core.database.entity.MetadataEntity
import org.meshtastic.core.database.entity.MyNodeEntity
import org.meshtastic.core.database.entity.NodeEntity
@ -26,7 +26,7 @@ import org.meshtastic.core.di.CoroutineDispatchers
@Single
class SwitchingNodeInfoWriteDataSource(
private val dbManager: DatabaseManager,
private val dbManager: DatabaseProvider,
private val dispatchers: CoroutineDispatchers,
) : NodeInfoWriteDataSource {

View file

@ -30,7 +30,7 @@ import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.model.MeshLog
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.util.isLora
import org.meshtastic.core.repository.FromRadioPacketHandler

View file

@ -68,7 +68,7 @@ class MqttManagerImpl(
}
override fun handleMqttProxyMessage(message: MqttClientProxyMessage) {
val topic = message.topic ?: ""
val topic = message.topic
Logger.d { "[mqttClientProxyMessage] $topic" }
val retained = message.retained == true
when {

View file

@ -21,6 +21,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.NumberFormatter
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.repository.CommandSender
import org.meshtastic.core.repository.NeighborInfoHandler
@ -29,7 +30,6 @@ import org.meshtastic.core.repository.ServiceBroadcasts
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.NeighborInfo
import java.util.Locale
@Single
class NeighborInfoHandlerImpl(
@ -49,7 +49,7 @@ class NeighborInfoHandlerImpl(
val ni = NeighborInfo.ADAPTER.decode(payload)
// Store the last neighbor info from our connected radio
val from = packet.from ?: 0
val from = packet.from
if (from == nodeManager.myNodeNum) {
commandSender.lastNeighborInfo = ni
Logger.d { "Stored last neighbor info from connected radio" }
@ -76,7 +76,7 @@ class NeighborInfoHandlerImpl(
val elapsedMs = nowMillis - start
val seconds = elapsedMs / MILLIS_PER_SECOND
Logger.i { "Neighbor info $requestId complete in $seconds s" }
String.format(Locale.US, "%s\n\nDuration: %.1f s", formatted, seconds)
"$formatted\n\nDuration: ${NumberFormatter.format(seconds, 1)} s"
} else {
formatted
}

View file

@ -319,10 +319,10 @@ class NodeManagerImpl(
longitude = longitude,
altitude = position.altitude ?: 0,
time = position.time,
satellitesInView = position.sats_in_view ?: 0,
satellitesInView = position.sats_in_view,
groundSpeed = position.ground_speed ?: 0,
groundTrack = position.ground_track ?: 0,
precisionBits = position.precision_bits ?: 0,
precisionBits = position.precision_bits,
)
.takeIf { latitude != 0.0 || longitude != 0.0 },
snr = snr,

View file

@ -31,9 +31,9 @@ import kotlinx.coroutines.withTimeoutOrNull
import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MeshLog
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.model.RadioNotConnectedException
import org.meshtastic.core.model.util.toOneLineString

View file

@ -21,6 +21,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.NumberFormatter
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.data.repository.TracerouteSnapshotRepository
@ -34,7 +35,6 @@ import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.repository.TracerouteHandler
import org.meshtastic.proto.MeshPacket
import java.util.Locale
@Single
class TracerouteHandlerImpl(
@ -83,7 +83,7 @@ class TracerouteHandlerImpl(
val elapsedMs = nowMillis - start
val seconds = elapsedMs / MILLIS_PER_SECOND
Logger.i { "Traceroute $requestId complete in $seconds s" }
val durationText = "Duration: %.1f s".format(Locale.US, seconds)
val durationText = "Duration: ${NumberFormatter.format(seconds, 1)} s"
"$full\n\n$durationText"
} else {
full

View file

@ -28,9 +28,11 @@ import kotlinx.coroutines.withContext
import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.data.datasource.NodeInfoReadDataSource
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.database.DatabaseProvider
import org.meshtastic.core.database.entity.asEntity
import org.meshtastic.core.database.entity.asExternalModel
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.model.MeshLog
import org.meshtastic.core.repository.MeshLogPrefs
import org.meshtastic.core.repository.MeshLogRepository
import org.meshtastic.core.repository.MeshLogRepository.Companion.DEFAULT_MAX_LOGS
@ -48,19 +50,23 @@ import org.meshtastic.proto.Telemetry
@Suppress("TooManyFunctions")
@Single
class MeshLogRepositoryImpl(
private val dbManager: DatabaseManager,
private val dbManager: DatabaseProvider,
private val dispatchers: CoroutineDispatchers,
private val meshLogPrefs: MeshLogPrefs,
private val nodeInfoReadDataSource: NodeInfoReadDataSource,
) : MeshLogRepository {
/** Retrieves all [MeshLog]s in the database, up to [maxItem]. */
override fun getAllLogs(maxItem: Int): Flow<List<MeshLog>> =
dbManager.currentDb.flatMapLatest { it.meshLogDao().getAllLogs(maxItem) }.flowOn(dispatchers.io)
override fun getAllLogs(maxItem: Int): Flow<List<MeshLog>> = dbManager.currentDb
.flatMapLatest { it.meshLogDao().getAllLogs(maxItem) }
.map { list -> list.map { it.asExternalModel() } }
.flowOn(dispatchers.io)
/** Retrieves all [MeshLog]s in the database in the order they were received. */
override fun getAllLogsInReceiveOrder(maxItem: Int): Flow<List<MeshLog>> =
dbManager.currentDb.flatMapLatest { it.meshLogDao().getAllLogsInReceiveOrder(maxItem) }.flowOn(dispatchers.io)
override fun getAllLogsInReceiveOrder(maxItem: Int): Flow<List<MeshLog>> = dbManager.currentDb
.flatMapLatest { it.meshLogDao().getAllLogsInReceiveOrder(maxItem) }
.map { list -> list.map { it.asExternalModel() } }
.flowOn(dispatchers.io)
/** Retrieves all [MeshLog]s in the database without any limit. */
override fun getAllLogsUnbounded(): Flow<List<MeshLog>> = getAllLogs(Int.MAX_VALUE)
@ -68,6 +74,7 @@ class MeshLogRepositoryImpl(
/** Retrieves all [MeshLog]s associated with a specific [nodeNum] and [portNum]. */
override fun getLogsFrom(nodeNum: Int, portNum: Int): Flow<List<MeshLog>> = dbManager.currentDb
.flatMapLatest { it.meshLogDao().getLogsFrom(nodeNum, portNum, DEFAULT_MAX_LOGS) }
.map { list -> list.map { it.asExternalModel() } }
.distinctUntilChanged()
.flowOn(dispatchers.io)
@ -81,7 +88,7 @@ class MeshLogRepositoryImpl(
dbManager.currentDb
.flatMapLatest { it.meshLogDao().getLogsFrom(logId, PortNum.TELEMETRY_APP.value, DEFAULT_MAX_LOGS) }
.distinctUntilChanged()
.mapLatest { list -> list.mapNotNull(::parseTelemetryLog) }
.mapLatest { list -> list.map { it.asExternalModel() }.mapNotNull(::parseTelemetryLog) }
}
.flowOn(dispatchers.io)
@ -93,12 +100,14 @@ class MeshLogRepositoryImpl(
override fun getRequestLogs(targetNodeNum: Int, portNum: PortNum): Flow<List<MeshLog>> = dbManager.currentDb
.flatMapLatest { it.meshLogDao().getLogsFrom(MeshLog.NODE_NUM_LOCAL, portNum.value, DEFAULT_MAX_LOGS) }
.map { list ->
list.filter { log ->
val packet = log.fromRadio.packet ?: return@filter false
log.fromNum == MeshLog.NODE_NUM_LOCAL &&
packet.to == targetNodeNum &&
packet.decoded?.want_response == true
}
list
.map { it.asExternalModel() }
.filter { log ->
val packet = log.fromRadio.packet ?: return@filter false
log.fromNum == MeshLog.NODE_NUM_LOCAL &&
packet.to == targetNodeNum &&
packet.decoded?.want_response == true
}
}
.distinctUntilChanged()
.conflate()
@ -141,13 +150,13 @@ class MeshLogRepositoryImpl(
/** Returns the cached [MyNodeInfo] from the system logs. */
override fun getMyNodeInfo(): Flow<MyNodeInfo?> = dbManager.currentDb
.flatMapLatest { db -> db.meshLogDao().getLogsFrom(MeshLog.NODE_NUM_LOCAL, 0, DEFAULT_MAX_LOGS) }
.mapLatest { list -> list.firstOrNull { it.myNodeInfo != null }?.myNodeInfo }
.mapLatest { list -> list.map { it.asExternalModel() }.firstOrNull { it.myNodeInfo != null }?.myNodeInfo }
.flowOn(dispatchers.io)
/** Persists a new log entry to the database if logging is enabled in preferences. */
override suspend fun insert(log: MeshLog) = withContext(dispatchers.io) {
if (!meshLogPrefs.loggingEnabled.value) return@withContext
dbManager.currentDb.value.meshLogDao().insert(log)
dbManager.currentDb.value.meshLogDao().insert(log.asEntity())
}
/** Clears all logs from the database. */

View file

@ -38,13 +38,13 @@ import org.koin.core.annotation.Named
import org.koin.core.annotation.Single
import org.meshtastic.core.data.datasource.NodeInfoReadDataSource
import org.meshtastic.core.data.datasource.NodeInfoWriteDataSource
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.database.entity.MetadataEntity
import org.meshtastic.core.database.entity.MyNodeEntity
import org.meshtastic.core.database.entity.NodeEntity
import org.meshtastic.core.datastore.LocalStatsDataSource
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MeshLog
import org.meshtastic.core.model.MyNodeInfo
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.NodeSortOption

View file

@ -27,7 +27,7 @@ import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.withContext
import okio.ByteString.Companion.toByteString
import org.koin.core.annotation.Single
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.database.DatabaseProvider
import org.meshtastic.core.database.entity.toReaction
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.model.ContactSettings
@ -45,7 +45,7 @@ import org.meshtastic.core.repository.PacketRepository as SharedPacketRepository
@Suppress("TooManyFunctions", "LongParameterList")
@Single
class PacketRepositoryImpl(private val dbManager: DatabaseManager, private val dispatchers: CoroutineDispatchers) :
class PacketRepositoryImpl(private val dbManager: DatabaseProvider, private val dispatchers: CoroutineDispatchers) :
SharedPacketRepository {
override fun getWaypoints(): Flow<List<DataPacket>> = dbManager.currentDb

View file

@ -20,12 +20,15 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.withContext
import org.koin.core.annotation.Single
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.database.DatabaseProvider
import org.meshtastic.core.database.entity.QuickChatAction
import org.meshtastic.core.di.CoroutineDispatchers
@Single
class QuickChatActionRepository(private val dbManager: DatabaseManager, private val dispatchers: CoroutineDispatchers) {
class QuickChatActionRepository(
private val dbManager: DatabaseProvider,
private val dispatchers: CoroutineDispatchers,
) {
fun getAllActions() = dbManager.currentDb.flatMapLatest { it.quickChatActionDao().getAll() }.flowOn(dispatchers.io)
suspend fun upsert(action: QuickChatAction) =

View file

@ -24,14 +24,14 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.withContext
import org.koin.core.annotation.Single
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.database.DatabaseProvider
import org.meshtastic.core.database.entity.TracerouteNodePositionEntity
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.proto.Position
@Single
class TracerouteSnapshotRepository(
private val dbManager: DatabaseManager,
private val dbManager: DatabaseProvider,
private val dispatchers: CoroutineDispatchers,
) {

View file

@ -91,7 +91,7 @@ class MeshConnectionManagerImplTest {
@Before
fun setUp() {
mockkStatic("org.meshtastic.core.resources.ContextExtKt")
mockkStatic("org.meshtastic.core.resources.GetStringKt")
every { getString(any()) } returns "Mocked String"
every { getString(any(), *anyVararg()) } returns "Mocked String"
@ -128,7 +128,7 @@ class MeshConnectionManagerImplTest {
@After
fun tearDown() {
unmockkStatic("org.meshtastic.core.resources.ContextExtKt")
unmockkStatic("org.meshtastic.core.resources.GetStringKt")
}
@Test

View file

@ -19,7 +19,6 @@ package org.meshtastic.core.data.manager
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
@ -80,12 +79,6 @@ class MeshDataHandlerTest {
@OptIn(ExperimentalCoroutinesApi::class)
@Before
fun setUp() {
mockkStatic(android.util.Log::class)
every { android.util.Log.d(any(), any()) } returns 0
every { android.util.Log.i(any(), any()) } returns 0
every { android.util.Log.w(any(), any<String>()) } returns 0
every { android.util.Log.e(any(), any()) } returns 0
meshDataHandler =
MeshDataHandlerImpl(
nodeManager,

View file

@ -26,8 +26,8 @@ import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.MeshLog
import org.meshtastic.core.repository.MeshLogRepository
import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.repository.RadioInterfaceService

View file

@ -30,12 +30,12 @@ import org.junit.Assert.assertNotNull
import org.junit.Test
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.data.datasource.NodeInfoReadDataSource
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.database.DatabaseProvider
import org.meshtastic.core.database.MeshtasticDatabase
import org.meshtastic.core.database.dao.MeshLogDao
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.database.entity.MyNodeEntity
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.model.MeshLog
import org.meshtastic.core.repository.MeshLogPrefs
import org.meshtastic.proto.Data
import org.meshtastic.proto.EnvironmentMetrics
@ -44,10 +44,11 @@ import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.PortNum
import org.meshtastic.proto.Telemetry
import kotlin.uuid.Uuid
import org.meshtastic.core.database.entity.MeshLog as MeshLogEntity
class MeshLogRepositoryTest {
private val dbManager: DatabaseManager = mockk()
private val dbManager: DatabaseProvider = mockk()
private val appDatabase: MeshtasticDatabase = mockk()
private val meshLogDao: MeshLogDao = mockk()
private val meshLogPrefs: MeshLogPrefs = mockk()
@ -127,7 +128,7 @@ class MeshLogRepositoryTest {
val logs =
listOf(
// Valid request
MeshLog(
MeshLogEntity(
uuid = "1",
message_type = "Packet",
received_date = nowMillis,
@ -141,7 +142,7 @@ class MeshLogRepositoryTest {
),
),
// Wrong target
MeshLog(
MeshLogEntity(
uuid = "2",
message_type = "Packet",
received_date = nowMillis,
@ -155,7 +156,7 @@ class MeshLogRepositoryTest {
),
),
// Not a request (want_response = false)
MeshLog(
MeshLogEntity(
uuid = "3",
message_type = "Packet",
received_date = nowMillis,
@ -169,7 +170,7 @@ class MeshLogRepositoryTest {
),
),
// Wrong fromNum
MeshLog(
MeshLogEntity(
uuid = "4",
message_type = "Packet",
received_date = nowMillis,

View file

@ -38,10 +38,10 @@ import org.junit.Before
import org.junit.Test
import org.meshtastic.core.data.datasource.NodeInfoReadDataSource
import org.meshtastic.core.data.datasource.NodeInfoWriteDataSource
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.database.entity.MyNodeEntity
import org.meshtastic.core.datastore.LocalStatsDataSource
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.model.MeshLog
@OptIn(ExperimentalCoroutinesApi::class)
class NodeRepositoryTest {