mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: Accurately display outgoing diagnostic packets (#4569)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
6a244316b2
commit
c690ddc7ea
18 changed files with 922 additions and 381 deletions
|
|
@ -19,14 +19,16 @@ package org.meshtastic.core.data.repository
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.data.datasource.NodeInfoReadDataSource
|
||||
import org.meshtastic.core.database.DatabaseManager
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.model.util.TimeConstants
|
||||
import org.meshtastic.core.model.util.nowMillis
|
||||
import org.meshtastic.core.prefs.meshlog.MeshLogPrefs
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
|
|
@ -34,33 +36,83 @@ import org.meshtastic.proto.MyNodeInfo
|
|||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.Telemetry
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Repository for managing and retrieving logs from the local database.
|
||||
*
|
||||
* This repository provides methods for inserting, deleting, and querying logs, including specialized methods for
|
||||
* telemetry and traceroute data.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
@Singleton
|
||||
class MeshLogRepository
|
||||
@Inject
|
||||
constructor(
|
||||
private val dbManager: DatabaseManager,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
private val meshLogPrefs: MeshLogPrefs,
|
||||
private val nodeInfoReadDataSource: NodeInfoReadDataSource,
|
||||
) {
|
||||
fun getAllLogs(maxItems: Int = MAX_ITEMS): Flow<List<MeshLog>> =
|
||||
dbManager.currentDb.flatMapLatest { it.meshLogDao().getAllLogs(maxItems) }.flowOn(dispatchers.io).conflate()
|
||||
|
||||
fun getAllLogsUnbounded(): Flow<List<MeshLog>> = dbManager.currentDb
|
||||
.flatMapLatest { it.meshLogDao().getAllLogs(Int.MAX_VALUE) }
|
||||
.flowOn(dispatchers.io)
|
||||
.conflate()
|
||||
|
||||
fun getAllLogsInReceiveOrder(maxItems: Int = MAX_ITEMS): Flow<List<MeshLog>> = dbManager.currentDb
|
||||
.flatMapLatest { it.meshLogDao().getAllLogsInReceiveOrder(maxItems) }
|
||||
/** Retrieves all [MeshLog]s in the database, up to [maxItem]. */
|
||||
fun getAllLogs(maxItem: Int = MAX_MESH_PACKETS): Flow<List<MeshLog>> =
|
||||
dbManager.currentDb.flatMapLatest { it.meshLogDao().getAllLogs(maxItem) }.flowOn(dispatchers.io)
|
||||
|
||||
/** Retrieves all [MeshLog]s in the database in the order they were received. */
|
||||
fun getAllLogsInReceiveOrder(maxItem: Int = MAX_MESH_PACKETS): Flow<List<MeshLog>> =
|
||||
dbManager.currentDb.flatMapLatest { it.meshLogDao().getAllLogsInReceiveOrder(maxItem) }.flowOn(dispatchers.io)
|
||||
|
||||
/** Retrieves all [MeshLog]s in the database without any limit. */
|
||||
fun getAllLogsUnbounded(): Flow<List<MeshLog>> = getAllLogs(Int.MAX_VALUE)
|
||||
|
||||
/** Retrieves all [MeshLog]s associated with a specific [nodeNum] and [portNum]. */
|
||||
fun getLogsFrom(nodeNum: Int, portNum: Int): Flow<List<MeshLog>> = dbManager.currentDb
|
||||
.flatMapLatest { it.meshLogDao().getLogsFrom(nodeNum, portNum, MAX_MESH_PACKETS) }
|
||||
.distinctUntilChanged()
|
||||
.flowOn(dispatchers.io)
|
||||
|
||||
/** Retrieves all [MeshLog]s containing [MeshPacket]s for a specific [nodeNum]. */
|
||||
fun getMeshPacketsFrom(nodeNum: Int, portNum: Int = -1): Flow<List<MeshPacket>> =
|
||||
getLogsFrom(nodeNum, portNum).map { list -> list.mapNotNull { it.fromRadio.packet } }.flowOn(dispatchers.io)
|
||||
|
||||
/** Retrieves telemetry history for a specific node, automatically handling local node redirection. */
|
||||
fun getTelemetryFrom(nodeNum: Int): Flow<List<Telemetry>> = effectiveLogId(nodeNum)
|
||||
.flatMapLatest { logId ->
|
||||
dbManager.currentDb
|
||||
.flatMapLatest { it.meshLogDao().getLogsFrom(logId, PortNum.TELEMETRY_APP.value, MAX_MESH_PACKETS) }
|
||||
.distinctUntilChanged()
|
||||
.mapLatest { list -> list.mapNotNull(::parseTelemetryLog) }
|
||||
}
|
||||
.flowOn(dispatchers.io)
|
||||
|
||||
/**
|
||||
* Retrieves all outgoing request logs for a specific [targetNodeNum] and [portNum].
|
||||
*
|
||||
* A request log is defined as an outgoing packet (`fromNum = 0`) where `want_response` is true.
|
||||
*/
|
||||
fun getRequestLogs(targetNodeNum: Int, portNum: PortNum): Flow<List<MeshLog>> = dbManager.currentDb
|
||||
.flatMapLatest { it.meshLogDao().getLogsFrom(MeshLog.NODE_NUM_LOCAL, portNum.value, MAX_MESH_PACKETS) }
|
||||
.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
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.conflate()
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private fun parseTelemetryLog(log: MeshLog): Telemetry? = runCatching {
|
||||
val payload = log.fromRadio.packet?.decoded?.payload ?: return@runCatching null
|
||||
val telemetry = Telemetry.ADAPTER.decode(payload)
|
||||
val decoded = log.fromRadio.packet?.decoded ?: return@runCatching null
|
||||
// Requests for telemetry (want_response = true) should not be logged as data points.
|
||||
if (decoded.want_response == true) return@runCatching null
|
||||
|
||||
val telemetry = Telemetry.ADAPTER.decode(decoded.payload)
|
||||
telemetry.copy(
|
||||
time = (log.received_date / MILLIS_TO_SECONDS).toInt(),
|
||||
time = (log.received_date / MILLIS_PER_SEC).toInt(),
|
||||
environment_metrics =
|
||||
telemetry.environment_metrics?.let { metrics ->
|
||||
metrics.copy(
|
||||
|
|
@ -81,63 +133,47 @@ constructor(
|
|||
}
|
||||
.getOrNull()
|
||||
|
||||
fun getTelemetryFrom(nodeNum: Int): Flow<List<Telemetry>> = dbManager.currentDb
|
||||
.flatMapLatest { it.meshLogDao().getLogsFrom(nodeNum, PortNum.TELEMETRY_APP.value, MAX_MESH_PACKETS) }
|
||||
/** Returns a flow that maps a [nodeNum] to [MeshLog.NODE_NUM_LOCAL] if it is the locally connected node. */
|
||||
private fun effectiveLogId(nodeNum: Int): Flow<Int> = nodeInfoReadDataSource
|
||||
.myNodeInfoFlow()
|
||||
.map { info -> if (nodeNum == info?.myNodeNum) MeshLog.NODE_NUM_LOCAL else nodeNum }
|
||||
.distinctUntilChanged()
|
||||
.mapLatest { list -> list.mapNotNull(::parseTelemetryLog) }
|
||||
.flowOn(dispatchers.io)
|
||||
|
||||
fun getLogsFrom(
|
||||
nodeNum: Int,
|
||||
portNum: Int = PortNum.UNKNOWN_APP.value,
|
||||
maxItem: Int = MAX_MESH_PACKETS,
|
||||
): Flow<List<MeshLog>> = dbManager.currentDb
|
||||
.flatMapLatest { it.meshLogDao().getLogsFrom(nodeNum, portNum, maxItem) }
|
||||
.distinctUntilChanged()
|
||||
.flowOn(dispatchers.io)
|
||||
|
||||
/*
|
||||
* Retrieves MeshPackets matching 'nodeNum' and 'portNum'.
|
||||
* If 'portNum' is not specified, returns all MeshPackets. Otherwise, filters by 'portNum'.
|
||||
*/
|
||||
fun getMeshPacketsFrom(nodeNum: Int, portNum: Int = PortNum.UNKNOWN_APP.value): Flow<List<MeshPacket>> =
|
||||
getLogsFrom(nodeNum, portNum)
|
||||
.mapLatest { list -> list.mapNotNull { it.fromRadio.packet } }
|
||||
.flowOn(dispatchers.io)
|
||||
|
||||
fun getMyNodeInfo(): Flow<MyNodeInfo?> = getLogsFrom(0, 0)
|
||||
/** Returns the cached [MyNodeInfo] from the system logs. */
|
||||
fun getMyNodeInfo(): Flow<MyNodeInfo?> = dbManager.currentDb
|
||||
.flatMapLatest { db -> db.meshLogDao().getLogsFrom(MeshLog.NODE_NUM_LOCAL, 0, MAX_MESH_PACKETS) }
|
||||
.mapLatest { list -> list.firstOrNull { it.myNodeInfo != null }?.myNodeInfo }
|
||||
.flowOn(dispatchers.io)
|
||||
|
||||
/** Persists a new log entry to the database if logging is enabled in preferences. */
|
||||
suspend fun insert(log: MeshLog) = withContext(dispatchers.io) {
|
||||
if (!meshLogPrefs.loggingEnabled) return@withContext
|
||||
dbManager.currentDb.value.meshLogDao().insert(log)
|
||||
}
|
||||
|
||||
/** Clears all logs from the database. */
|
||||
suspend fun deleteAll() = withContext(dispatchers.io) { dbManager.currentDb.value.meshLogDao().deleteAll() }
|
||||
|
||||
/** Deletes a specific log entry by its [uuid]. */
|
||||
suspend fun deleteLog(uuid: String) =
|
||||
withContext(dispatchers.io) { dbManager.currentDb.value.meshLogDao().deleteLog(uuid) }
|
||||
|
||||
suspend fun deleteLogs(nodeNum: Int, portNum: Int) =
|
||||
withContext(dispatchers.io) { dbManager.currentDb.value.meshLogDao().deleteLogs(nodeNum, portNum) }
|
||||
/** Deletes all logs associated with a specific [nodeNum] and [portNum]. */
|
||||
suspend fun deleteLogs(nodeNum: Int, portNum: Int) = withContext(dispatchers.io) {
|
||||
val myNodeNum = nodeInfoReadDataSource.myNodeInfoFlow().firstOrNull()?.myNodeNum
|
||||
val logId = if (nodeNum == myNodeNum) MeshLog.NODE_NUM_LOCAL else nodeNum
|
||||
dbManager.currentDb.value.meshLogDao().deleteLogs(logId, portNum)
|
||||
}
|
||||
|
||||
/** Prunes the log database based on the configured [retentionDays]. */
|
||||
@Suppress("MagicNumber")
|
||||
suspend fun deleteLogsOlderThan(retentionDays: Int) = withContext(dispatchers.io) {
|
||||
if (retentionDays == MeshLogPrefs.NEVER_CLEAR_RETENTION_DAYS) return@withContext
|
||||
|
||||
val cutoffTimestamp =
|
||||
if (retentionDays == MeshLogPrefs.ONE_HOUR_RETENTION_DAYS) {
|
||||
nowMillis - TimeConstants.ONE_HOUR.inWholeMilliseconds
|
||||
} else {
|
||||
nowMillis - (retentionDays * TimeConstants.ONE_DAY.inWholeMilliseconds)
|
||||
}
|
||||
dbManager.currentDb.value.meshLogDao().deleteOlderThan(cutoffTimestamp)
|
||||
val cutoffTime = nowMillis - (retentionDays.toLong() * 24 * 60 * 60 * 1000)
|
||||
dbManager.currentDb.value.meshLogDao().deleteOlderThan(cutoffTime)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_ITEMS = 500
|
||||
private const val MAX_MESH_PACKETS = 10000
|
||||
private const val MILLIS_TO_SECONDS = 1000
|
||||
private const val MAX_MESH_PACKETS = 5000
|
||||
private const val MILLIS_PER_SEC = 1000L
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.SharingStarted
|
|||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
|
@ -35,6 +36,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
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
|
||||
|
|
@ -49,6 +51,7 @@ import org.meshtastic.proto.User
|
|||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** Repository for managing node-related data, including hardware info, node database, and identity. */
|
||||
@Singleton
|
||||
@Suppress("TooManyFunctions")
|
||||
class NodeRepository
|
||||
|
|
@ -59,24 +62,26 @@ constructor(
|
|||
private val nodeInfoWriteDataSource: NodeInfoWriteDataSource,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) {
|
||||
// hardware info about our local device (can be null)
|
||||
/** Hardware info about our local device (can be null if not connected). */
|
||||
val myNodeInfo: StateFlow<MyNodeEntity?> =
|
||||
nodeInfoReadDataSource
|
||||
.myNodeInfoFlow()
|
||||
.flowOn(dispatchers.io)
|
||||
.stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, null)
|
||||
|
||||
// our node info
|
||||
private val _ourNodeInfo = MutableStateFlow<Node?>(null)
|
||||
|
||||
/** Information about the locally connected node, as seen from the mesh. */
|
||||
val ourNodeInfo: StateFlow<Node?>
|
||||
get() = _ourNodeInfo
|
||||
|
||||
// The unique userId of our node
|
||||
private val _myId = MutableStateFlow<String?>(null)
|
||||
|
||||
/** The unique userId (hex string) of our local node. */
|
||||
val myId: StateFlow<String?>
|
||||
get() = _myId
|
||||
|
||||
// A map from nodeNum to Node
|
||||
/** A reactive map from nodeNum to [Node] objects, representing the entire mesh. */
|
||||
val nodeDBbyNum: StateFlow<Map<Int, Node>> =
|
||||
nodeInfoReadDataSource
|
||||
.nodeDBbyNumFlow()
|
||||
|
|
@ -102,14 +107,25 @@ constructor(
|
|||
.launchIn(processLifecycle.coroutineScope)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the node number used for log queries. Maps [nodeNum] to [MeshLog.NODE_NUM_LOCAL] (0) if it is the locally
|
||||
* connected node.
|
||||
*/
|
||||
fun effectiveLogNodeId(nodeNum: Int): Flow<Int> = myNodeInfo
|
||||
.map { info -> if (nodeNum == info?.myNodeNum) MeshLog.NODE_NUM_LOCAL else nodeNum }
|
||||
.distinctUntilChanged()
|
||||
|
||||
fun getNodeDBbyNum() =
|
||||
nodeInfoReadDataSource.nodeDBbyNumFlow().map { map -> map.mapValues { (_, it) -> it.toEntity() } }
|
||||
|
||||
/** Returns the [Node] associated with a given [userId]. Falls back to a generic node if not found. */
|
||||
fun getNode(userId: String): Node = nodeDBbyNum.value.values.find { it.user.id == userId }
|
||||
?: Node(num = DataPacket.idToDefaultNodeNum(userId) ?: 0, user = getUser(userId))
|
||||
|
||||
/** Returns the [User] info for a given [nodeNum]. */
|
||||
fun getUser(nodeNum: Int): User = getUser(DataPacket.nodeNumToDefaultId(nodeNum))
|
||||
|
||||
/** Returns the [User] info for a given [userId]. Falls back to a generic user if not found. */
|
||||
fun getUser(userId: String): User = nodeDBbyNum.value.values.find { it.user.id == userId }?.user
|
||||
?: User(
|
||||
id = userId,
|
||||
|
|
@ -128,6 +144,7 @@ constructor(
|
|||
hw_model = HardwareModel.UNSET,
|
||||
)
|
||||
|
||||
/** Returns a flow of nodes filtered and sorted according to the parameters. */
|
||||
fun getNodes(
|
||||
sort: NodeSortOption = NodeSortOption.LAST_HEARD,
|
||||
filter: String = "",
|
||||
|
|
@ -146,21 +163,27 @@ constructor(
|
|||
.flowOn(dispatchers.io)
|
||||
.conflate()
|
||||
|
||||
/** Upserts a [NodeEntity] to the database. */
|
||||
suspend fun upsert(node: NodeEntity) = withContext(dispatchers.io) { nodeInfoWriteDataSource.upsert(node) }
|
||||
|
||||
/** Installs initial configuration data (local info and remote nodes) into the database. */
|
||||
suspend fun installConfig(mi: MyNodeEntity, nodes: List<NodeEntity>) =
|
||||
withContext(dispatchers.io) { nodeInfoWriteDataSource.installConfig(mi, nodes) }
|
||||
|
||||
/** Deletes all nodes from the database, optionally preserving favorites. */
|
||||
suspend fun clearNodeDB(preserveFavorites: Boolean = false) =
|
||||
withContext(dispatchers.io) { nodeInfoWriteDataSource.clearNodeDB(preserveFavorites) }
|
||||
|
||||
/** Clears the local node's connection info. */
|
||||
suspend fun clearMyNodeInfo() = withContext(dispatchers.io) { nodeInfoWriteDataSource.clearMyNodeInfo() }
|
||||
|
||||
/** Deletes a node and its metadata by [num]. */
|
||||
suspend fun deleteNode(num: Int) = withContext(dispatchers.io) {
|
||||
nodeInfoWriteDataSource.deleteNode(num)
|
||||
nodeInfoWriteDataSource.deleteMetadata(num)
|
||||
}
|
||||
|
||||
/** Deletes multiple nodes and their metadata. */
|
||||
suspend fun deleteNodes(nodeNums: List<Int>) = withContext(dispatchers.io) {
|
||||
nodeInfoWriteDataSource.deleteNodes(nodeNums)
|
||||
nodeNums.forEach { nodeInfoWriteDataSource.deleteMetadata(it) }
|
||||
|
|
@ -172,9 +195,11 @@ constructor(
|
|||
suspend fun getUnknownNodes(): List<NodeEntity> =
|
||||
withContext(dispatchers.io) { nodeInfoReadDataSource.getUnknownNodes() }
|
||||
|
||||
/** Persists hardware metadata for a node. */
|
||||
suspend fun insertMetadata(metadata: MetadataEntity) =
|
||||
withContext(dispatchers.io) { nodeInfoWriteDataSource.upsert(metadata) }
|
||||
|
||||
/** Flow emitting the count of nodes currently considered "online". */
|
||||
val onlineNodeCount: Flow<Int> =
|
||||
nodeInfoReadDataSource
|
||||
.nodeDBbyNumFlow()
|
||||
|
|
@ -182,6 +207,7 @@ constructor(
|
|||
.flowOn(dispatchers.io)
|
||||
.conflate()
|
||||
|
||||
/** Flow emitting the total number of nodes in the database. */
|
||||
val totalNodeCount: Flow<Int> =
|
||||
nodeInfoReadDataSource
|
||||
.nodeDBbyNumFlow()
|
||||
|
|
@ -189,6 +215,7 @@ constructor(
|
|||
.flowOn(dispatchers.io)
|
||||
.conflate()
|
||||
|
||||
/** Updates the personal notes field for a node. */
|
||||
suspend fun setNodeNotes(num: Int, notes: String) =
|
||||
withContext(dispatchers.io) { nodeInfoWriteDataSource.setNodeNotes(num, notes) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,19 +16,24 @@
|
|||
*/
|
||||
package org.meshtastic.core.data.repository
|
||||
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.data.datasource.NodeInfoReadDataSource
|
||||
import org.meshtastic.core.database.DatabaseManager
|
||||
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.util.nowMillis
|
||||
import org.meshtastic.core.prefs.meshlog.MeshLogPrefs
|
||||
|
|
@ -46,14 +51,16 @@ class MeshLogRepositoryTest {
|
|||
private val appDatabase: MeshtasticDatabase = mockk()
|
||||
private val meshLogDao: MeshLogDao = mockk()
|
||||
private val meshLogPrefs: MeshLogPrefs = mockk()
|
||||
private val nodeInfoReadDataSource: NodeInfoReadDataSource = mockk()
|
||||
private val testDispatcher = UnconfinedTestDispatcher()
|
||||
private val dispatchers = CoroutineDispatchers(main = testDispatcher, io = testDispatcher, default = testDispatcher)
|
||||
|
||||
private val repository = MeshLogRepository(dbManager, dispatchers, meshLogPrefs)
|
||||
private val repository = MeshLogRepository(dbManager, dispatchers, meshLogPrefs, nodeInfoReadDataSource)
|
||||
|
||||
init {
|
||||
every { dbManager.currentDb } returns MutableStateFlow(appDatabase)
|
||||
every { appDatabase.meshLogDao() } returns meshLogDao
|
||||
every { nodeInfoReadDataSource.myNodeInfoFlow() } returns MutableStateFlow(null)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -110,4 +117,107 @@ class MeshLogRepositoryTest {
|
|||
// Should be NaN as per repository logic for missing fields
|
||||
assertEquals(Float.NaN, resultMetrics?.temperature ?: 0f, 0.01f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getRequestLogs filters correctly`() = runTest(testDispatcher) {
|
||||
val targetNode = 123
|
||||
val otherNode = 456
|
||||
val port = PortNum.TRACEROUTE_APP
|
||||
|
||||
val logs =
|
||||
listOf(
|
||||
// Valid request
|
||||
MeshLog(
|
||||
uuid = "1",
|
||||
message_type = "Packet",
|
||||
received_date = nowMillis,
|
||||
raw_message = "",
|
||||
fromNum = 0,
|
||||
portNum = port.value,
|
||||
fromRadio =
|
||||
FromRadio(
|
||||
packet =
|
||||
MeshPacket(to = targetNode, decoded = Data(portnum = port, want_response = true)),
|
||||
),
|
||||
),
|
||||
// Wrong target
|
||||
MeshLog(
|
||||
uuid = "2",
|
||||
message_type = "Packet",
|
||||
received_date = nowMillis,
|
||||
raw_message = "",
|
||||
fromNum = 0,
|
||||
portNum = port.value,
|
||||
fromRadio =
|
||||
FromRadio(
|
||||
packet =
|
||||
MeshPacket(to = otherNode, decoded = Data(portnum = port, want_response = true)),
|
||||
),
|
||||
),
|
||||
// Not a request (want_response = false)
|
||||
MeshLog(
|
||||
uuid = "3",
|
||||
message_type = "Packet",
|
||||
received_date = nowMillis,
|
||||
raw_message = "",
|
||||
fromNum = 0,
|
||||
portNum = port.value,
|
||||
fromRadio =
|
||||
FromRadio(
|
||||
packet =
|
||||
MeshPacket(to = targetNode, decoded = Data(portnum = port, want_response = false)),
|
||||
),
|
||||
),
|
||||
// Wrong fromNum
|
||||
MeshLog(
|
||||
uuid = "4",
|
||||
message_type = "Packet",
|
||||
received_date = nowMillis,
|
||||
raw_message = "",
|
||||
fromNum = 789,
|
||||
portNum = port.value,
|
||||
fromRadio =
|
||||
FromRadio(
|
||||
packet =
|
||||
MeshPacket(to = targetNode, decoded = Data(portnum = port, want_response = true)),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
every { meshLogDao.getLogsFrom(0, port.value, any()) } returns MutableStateFlow(logs)
|
||||
|
||||
val result = repository.getRequestLogs(targetNode, port).first()
|
||||
|
||||
assertEquals(1, result.size)
|
||||
assertEquals("1", result[0].uuid)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deleteLogs redirects local node number to NODE_NUM_LOCAL`() = runTest(testDispatcher) {
|
||||
val localNodeNum = 999
|
||||
val port = 100
|
||||
val myNodeEntity = mockk<MyNodeEntity>()
|
||||
every { myNodeEntity.myNodeNum } returns localNodeNum
|
||||
every { nodeInfoReadDataSource.myNodeInfoFlow() } returns MutableStateFlow(myNodeEntity)
|
||||
coEvery { meshLogDao.deleteLogs(any(), any()) } returns Unit
|
||||
|
||||
repository.deleteLogs(localNodeNum, port)
|
||||
|
||||
coVerify { meshLogDao.deleteLogs(MeshLog.NODE_NUM_LOCAL, port) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deleteLogs preserves remote node numbers`() = runTest(testDispatcher) {
|
||||
val localNodeNum = 999
|
||||
val remoteNodeNum = 888
|
||||
val port = 100
|
||||
val myNodeEntity = mockk<MyNodeEntity>()
|
||||
every { myNodeEntity.myNodeNum } returns localNodeNum
|
||||
every { nodeInfoReadDataSource.myNodeInfoFlow() } returns MutableStateFlow(myNodeEntity)
|
||||
coEvery { meshLogDao.deleteLogs(any(), any()) } returns Unit
|
||||
|
||||
repository.deleteLogs(remoteNodeNum, port)
|
||||
|
||||
coVerify { meshLogDao.deleteLogs(remoteNodeNum, port) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* Copyright (c) 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.repository
|
||||
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
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.di.CoroutineDispatchers
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class NodeRepositoryTest {
|
||||
|
||||
private val readDataSource: NodeInfoReadDataSource = mockk(relaxed = true)
|
||||
private val writeDataSource: NodeInfoWriteDataSource = mockk(relaxed = true)
|
||||
private val lifecycle: Lifecycle = mockk(relaxed = true)
|
||||
private val lifecycleScope: LifecycleCoroutineScope = mockk()
|
||||
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
private val dispatchers = CoroutineDispatchers(main = testDispatcher, io = testDispatcher, default = testDispatcher)
|
||||
|
||||
private val myNodeInfoFlow = MutableStateFlow<MyNodeEntity?>(null)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
mockkStatic("androidx.lifecycle.LifecycleKt")
|
||||
every { lifecycleScope.coroutineContext } returns testDispatcher + Job()
|
||||
every { lifecycle.coroutineScope } returns lifecycleScope
|
||||
every { readDataSource.myNodeInfoFlow() } returns myNodeInfoFlow
|
||||
every { readDataSource.nodeDBbyNumFlow() } returns MutableStateFlow(emptyMap())
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
private fun createMyNodeEntity(nodeNum: Int) = MyNodeEntity(
|
||||
myNodeNum = nodeNum,
|
||||
model = "model",
|
||||
firmwareVersion = "1.0",
|
||||
couldUpdate = false,
|
||||
shouldUpdate = false,
|
||||
currentPacketId = 0L,
|
||||
messageTimeoutMsec = 0,
|
||||
minAppVersion = 0,
|
||||
maxChannels = 0,
|
||||
hasWifi = false,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `effectiveLogNodeId maps local node number to NODE_NUM_LOCAL`() = runTest(testDispatcher) {
|
||||
val myNodeNum = 12345
|
||||
myNodeInfoFlow.value = createMyNodeEntity(myNodeNum)
|
||||
|
||||
val repository = NodeRepository(lifecycle, readDataSource, writeDataSource, dispatchers)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
val result = repository.effectiveLogNodeId(myNodeNum).filter { it == MeshLog.NODE_NUM_LOCAL }.first()
|
||||
|
||||
assertEquals(MeshLog.NODE_NUM_LOCAL, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `effectiveLogNodeId preserves remote node numbers`() = runTest(testDispatcher) {
|
||||
val myNodeNum = 12345
|
||||
val remoteNodeNum = 67890
|
||||
myNodeInfoFlow.value = createMyNodeEntity(myNodeNum)
|
||||
|
||||
val repository = NodeRepository(lifecycle, readDataSource, writeDataSource, dispatchers)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
val result = repository.effectiveLogNodeId(remoteNodeNum).first()
|
||||
|
||||
assertEquals(remoteNodeNum, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `effectiveLogNodeId updates when local node number changes`() = runTest(testDispatcher) {
|
||||
val firstNodeNum = 111
|
||||
val secondNodeNum = 222
|
||||
val targetNodeNum = 111
|
||||
|
||||
myNodeInfoFlow.value = createMyNodeEntity(firstNodeNum)
|
||||
val repository = NodeRepository(lifecycle, readDataSource, writeDataSource, dispatchers)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// Initially should be mapped to LOCAL because it matches
|
||||
assertEquals(
|
||||
MeshLog.NODE_NUM_LOCAL,
|
||||
repository.effectiveLogNodeId(targetNodeNum).filter { it == MeshLog.NODE_NUM_LOCAL }.first(),
|
||||
)
|
||||
|
||||
// Change local node num
|
||||
myNodeInfoFlow.value = createMyNodeEntity(secondNodeNum)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// Now it shouldn't match, so should return the original num
|
||||
assertEquals(
|
||||
targetNodeNum,
|
||||
repository.effectiveLogNodeId(targetNodeNum).filter { it == targetNodeNum }.first(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue