refactor: migrate :core:database to Room Kotlin Multiplatform (#4702)

This commit is contained in:
James Rich 2026-03-03 20:44:34 -06:00 committed by GitHub
parent 744db2d5bd
commit 6a858acb4a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 406 additions and 264 deletions

View file

@ -16,9 +16,8 @@
*/
package org.meshtastic.core.data.datasource
import dagger.Lazy
import kotlinx.coroutines.withContext
import org.meshtastic.core.database.dao.DeviceHardwareDao
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.database.entity.DeviceHardwareEntity
import org.meshtastic.core.database.entity.asEntity
import org.meshtastic.core.di.CoroutineDispatchers
@ -28,10 +27,11 @@ import javax.inject.Inject
class DeviceHardwareLocalDataSource
@Inject
constructor(
private val deviceHardwareDaoLazy: Lazy<DeviceHardwareDao>,
private val dbManager: DatabaseManager,
private val dispatchers: CoroutineDispatchers,
) {
private val deviceHardwareDao by lazy { deviceHardwareDaoLazy.get() }
private val deviceHardwareDao
get() = dbManager.currentDb.value.deviceHardwareDao()
suspend fun insertAllDeviceHardware(deviceHardware: List<NetworkDeviceHardware>) =
withContext(dispatchers.io) { deviceHardwareDao.insertAll(deviceHardware.map { it.asEntity() }) }

View file

@ -16,9 +16,8 @@
*/
package org.meshtastic.core.data.datasource
import dagger.Lazy
import kotlinx.coroutines.withContext
import org.meshtastic.core.database.dao.FirmwareReleaseDao
import org.meshtastic.core.database.DatabaseManager
import org.meshtastic.core.database.entity.FirmwareReleaseEntity
import org.meshtastic.core.database.entity.FirmwareReleaseType
import org.meshtastic.core.database.entity.asDeviceVersion
@ -30,10 +29,11 @@ import javax.inject.Inject
class FirmwareReleaseLocalDataSource
@Inject
constructor(
private val firmwareReleaseDaoLazy: Lazy<FirmwareReleaseDao>,
private val dbManager: DatabaseManager,
private val dispatchers: CoroutineDispatchers,
) {
private val firmwareReleaseDao by lazy { firmwareReleaseDaoLazy.get() }
private val firmwareReleaseDao
get() = dbManager.currentDb.value.firmwareReleaseDao()
suspend fun insertFirmwareReleases(
firmwareReleases: List<NetworkFirmwareRelease>,

View file

@ -0,0 +1,32 @@
/*
* 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.core.data.di
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.meshtastic.core.database.DatabaseManager
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
interface DatabaseModule {
@Binds @Singleton
fun bindDatabaseManager(impl: DatabaseManager): org.meshtastic.core.repository.DatabaseManager
}

View file

@ -201,10 +201,12 @@ constructor(
val currentPosition =
when {
provideLocation && position.isValid() -> position
else ->
provideLocation ->
nodeManager.nodeDBbyNodeNum[myNodeNum]?.position?.let { Position(it) }?.takeIf { it.isValid() }
?: Position(0.0, 0.0, 0)
else -> Position(0.0, 0.0, 0)
}
currentPosition?.let { commandSender.requestPosition(destNum, it) }
commandSender.requestPosition(destNum, currentPosition)
}
}

View file

@ -440,11 +440,13 @@ constructor(
}
}
}
environment != null -> nextNode = nextNode.copy(environmentMetrics = environment)
power != null -> nextNode = nextNode.copy(powerMetrics = power)
}
nextNode
val telemetryTime = if (t.time != 0) t.time else nextNode.lastHeard
val newLastHeard = maxOf(nextNode.lastHeard, telemetryTime)
nextNode.copy(lastHeard = newLastHeard)
}
}

View file

@ -192,23 +192,43 @@ constructor(
}
override fun handleReceivedPosition(fromNum: Int, myNodeNum: Int, p: ProtoPosition, defaultTime: Long) {
if (myNodeNum == fromNum && (p.latitude_i ?: 0) == 0 && (p.longitude_i ?: 0) == 0) {
Logger.d { "Ignoring nop position update for the local node" }
} else {
updateNode(fromNum) { node ->
node.copy(position = p.copy(time = if (p.time != 0) p.time else (defaultTime / TIME_MS_TO_S).toInt()))
}
val isZeroPos = (p.latitude_i ?: 0) == 0 && (p.longitude_i ?: 0) == 0
@Suppress("ComplexCondition")
if (myNodeNum == fromNum && isZeroPos && p.sats_in_view == 0 && p.time == 0) {
Logger.d { "Ignoring empty position update for the local node" }
return
}
updateNode(fromNum) { node ->
val posTime = if (p.time != 0) p.time else (defaultTime / TIME_MS_TO_S).toInt()
val newLastHeard = maxOf(node.lastHeard, posTime)
val newPos =
if (isZeroPos) {
p.copy(
time = posTime,
latitude_i = node.position.latitude_i,
longitude_i = node.position.longitude_i,
altitude = p.altitude ?: node.position.altitude,
sats_in_view = p.sats_in_view,
)
} else {
p.copy(time = posTime)
}
node.copy(position = newPos, lastHeard = newLastHeard)
}
}
override fun handleReceivedTelemetry(fromNum: Int, telemetry: Telemetry) {
updateNode(fromNum) { node ->
when {
telemetry.device_metrics != null -> node.copy(deviceMetrics = telemetry.device_metrics!!)
telemetry.environment_metrics != null -> node.copy(environmentMetrics = telemetry.environment_metrics!!)
telemetry.power_metrics != null -> node.copy(powerMetrics = telemetry.power_metrics!!)
else -> node
}
var nextNode = node
telemetry.device_metrics?.let { nextNode = nextNode.copy(deviceMetrics = it) }
telemetry.environment_metrics?.let { nextNode = nextNode.copy(environmentMetrics = it) }
telemetry.power_metrics?.let { nextNode = nextNode.copy(powerMetrics = it) }
val telemetryTime = if (telemetry.time != 0) telemetry.time else node.lastHeard
val newLastHeard = maxOf(node.lastHeard, telemetryTime)
nextNode.copy(lastHeard = newLastHeard)
}
}

View file

@ -260,6 +260,8 @@ constructor(
num = num,
user = user,
position = position,
latitude = latitude,
longitude = longitude,
snr = snr,
rssi = rssi,
lastHeard = lastHeard,

View file

@ -105,6 +105,85 @@ class NodeManagerImplTest {
assertEquals(90.0, result.longitude, 0.0001)
}
@Test
fun `handleReceivedPosition with zero coordinates preserves last known location but updates satellites`() {
val nodeNum = 1234
val initialPosition = Position(latitude_i = 450000000, longitude_i = 900000000, sats_in_view = 10)
nodeManager.handleReceivedPosition(nodeNum, 9999, initialPosition, 1000000L)
// Receive "zero" position with new satellite count
val zeroPosition = Position(latitude_i = 0, longitude_i = 0, sats_in_view = 5, time = 1001)
nodeManager.handleReceivedPosition(nodeNum, 9999, zeroPosition, 1001000L)
val result = nodeManager.nodeDBbyNodeNum[nodeNum]
assertEquals(45.0, result!!.latitude, 0.0001)
assertEquals(90.0, result.longitude, 0.0001)
assertEquals(5, result.position.sats_in_view)
assertEquals(1001, result.lastHeard)
}
@Test
fun `handleReceivedPosition for local node ignores purely empty packets`() {
val myNum = 1111
val emptyPos = Position(latitude_i = 0, longitude_i = 0, sats_in_view = 0, time = 0)
nodeManager.handleReceivedPosition(myNum, myNum, emptyPos, 0)
val result = nodeManager.nodeDBbyNodeNum[myNum]
// Should still be a default/unset node if it didn't exist, or shouldn't have position
assertTrue(result == null || result.position.latitude_i == null)
}
@Test
fun `handleReceivedTelemetry updates lastHeard`() {
val nodeNum = 1234
nodeManager.updateNode(nodeNum) { it.copy(lastHeard = 1000) }
val telemetry =
org.meshtastic.proto.Telemetry(
time = 2000,
device_metrics = org.meshtastic.proto.DeviceMetrics(battery_level = 50),
)
nodeManager.handleReceivedTelemetry(nodeNum, telemetry)
val result = nodeManager.nodeDBbyNodeNum[nodeNum]
assertEquals(2000, result!!.lastHeard)
}
@Test
fun `handleReceivedTelemetry updates device metrics`() {
val nodeNum = 1234
val telemetry =
org.meshtastic.proto.Telemetry(
device_metrics = org.meshtastic.proto.DeviceMetrics(battery_level = 75, voltage = 3.8f),
)
nodeManager.handleReceivedTelemetry(nodeNum, telemetry)
val result = nodeManager.nodeDBbyNodeNum[nodeNum]
assertNotNull(result!!.deviceMetrics)
assertEquals(75, result.deviceMetrics.battery_level)
assertEquals(3.8f, result.deviceMetrics.voltage)
}
@Test
fun `handleReceivedTelemetry updates environment metrics`() {
val nodeNum = 1234
val telemetry =
org.meshtastic.proto.Telemetry(
environment_metrics =
org.meshtastic.proto.EnvironmentMetrics(temperature = 22.5f, relative_humidity = 45.0f),
)
nodeManager.handleReceivedTelemetry(nodeNum, telemetry)
val result = nodeManager.nodeDBbyNodeNum[nodeNum]
assertNotNull(result!!.environmentMetrics)
assertEquals(22.5f, result.environmentMetrics.temperature)
assertEquals(45.0f, result.environmentMetrics.relative_humidity)
}
@Test
fun `clear resets internal state`() {
nodeManager.updateNode(1234) { it.copy(user = it.user.copy(long_name = "Test")) }