feat(widget): Add Local Stats glance widget (#4642)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-02-25 13:39:00 -06:00 committed by GitHub
parent 692ad78c80
commit 9970d31520
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1256 additions and 24 deletions

View file

@ -42,11 +42,13 @@ import org.meshtastic.core.database.entity.MyNodeEntity
import org.meshtastic.core.database.entity.NodeEntity
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.database.model.NodeSortOption
import org.meshtastic.core.datastore.LocalStatsDataSource
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.di.ProcessLifecycle
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.util.onlineTimeThreshold
import org.meshtastic.proto.HardwareModel
import org.meshtastic.proto.LocalStats
import org.meshtastic.proto.User
import javax.inject.Inject
import javax.inject.Singleton
@ -57,10 +59,11 @@ import javax.inject.Singleton
class NodeRepository
@Inject
constructor(
@ProcessLifecycle processLifecycle: Lifecycle,
@ProcessLifecycle private val processLifecycle: Lifecycle,
private val nodeInfoReadDataSource: NodeInfoReadDataSource,
private val nodeInfoWriteDataSource: NodeInfoWriteDataSource,
private val dispatchers: CoroutineDispatchers,
private val localStatsDataSource: LocalStatsDataSource,
) {
/** Hardware info about our local device (can be null if not connected). */
val myNodeInfo: StateFlow<MyNodeEntity?> =
@ -81,6 +84,19 @@ constructor(
val myId: StateFlow<String?>
get() = _myId
/** The latest local stats telemetry received from the locally connected node. */
val localStats: StateFlow<LocalStats> =
localStatsDataSource.localStatsFlow.stateIn(
processLifecycle.coroutineScope,
SharingStarted.Eagerly,
LocalStats(),
)
/** Update the cached local stats telemetry. */
fun updateLocalStats(stats: LocalStats) {
processLifecycle.coroutineScope.launch { localStatsDataSource.setLocalStats(stats) }
}
/** A reactive map from nodeNum to [Node] objects, representing the entire mesh. */
val nodeDBbyNum: StateFlow<Map<Int, Node>> =
nodeInfoReadDataSource

View file

@ -40,6 +40,7 @@ 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
@OptIn(ExperimentalCoroutinesApi::class)
@ -49,6 +50,7 @@ class NodeRepositoryTest {
private val writeDataSource: NodeInfoWriteDataSource = mockk(relaxed = true)
private val lifecycle: Lifecycle = mockk(relaxed = true)
private val lifecycleScope: LifecycleCoroutineScope = mockk()
private val localStatsDataSource: LocalStatsDataSource = mockk(relaxed = true)
private val testDispatcher = StandardTestDispatcher()
private val dispatchers = CoroutineDispatchers(main = testDispatcher, io = testDispatcher, default = testDispatcher)
@ -88,7 +90,8 @@ class NodeRepositoryTest {
val myNodeNum = 12345
myNodeInfoFlow.value = createMyNodeEntity(myNodeNum)
val repository = NodeRepository(lifecycle, readDataSource, writeDataSource, dispatchers)
val repository =
NodeRepository(lifecycle, readDataSource, writeDataSource, dispatchers, localStatsDataSource)
testScheduler.runCurrent()
val result = repository.effectiveLogNodeId(myNodeNum).filter { it == MeshLog.NODE_NUM_LOCAL }.first()
@ -102,7 +105,8 @@ class NodeRepositoryTest {
val remoteNodeNum = 67890
myNodeInfoFlow.value = createMyNodeEntity(myNodeNum)
val repository = NodeRepository(lifecycle, readDataSource, writeDataSource, dispatchers)
val repository =
NodeRepository(lifecycle, readDataSource, writeDataSource, dispatchers, localStatsDataSource)
testScheduler.runCurrent()
val result = repository.effectiveLogNodeId(remoteNodeNum).first()
@ -117,7 +121,8 @@ class NodeRepositoryTest {
val targetNodeNum = 111
myNodeInfoFlow.value = createMyNodeEntity(firstNodeNum)
val repository = NodeRepository(lifecycle, readDataSource, writeDataSource, dispatchers)
val repository =
NodeRepository(lifecycle, readDataSource, writeDataSource, dispatchers, localStatsDataSource)
testScheduler.runCurrent()
// Initially should be mapped to LOCAL because it matches