kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
refactor: split `MetricsViewModel` state updates
- Consolidates `MetricViewModel` back to a single state flow - Introduces a `MutableStateFlow` for state updates, allowing more independent control - Moves `Telemetry`, `MeshPacket`, and config updates into separate coroutinespull/1376/head
rodzic
dcd5ca1c8e
commit
26f210047d
|
@ -6,51 +6,47 @@ import com.geeksville.mesh.CoroutineDispatchers
|
||||||
import com.geeksville.mesh.MeshProtos.MeshPacket
|
import com.geeksville.mesh.MeshProtos.MeshPacket
|
||||||
import com.geeksville.mesh.Portnums.PortNum
|
import com.geeksville.mesh.Portnums.PortNum
|
||||||
import com.geeksville.mesh.TelemetryProtos.Telemetry
|
import com.geeksville.mesh.TelemetryProtos.Telemetry
|
||||||
|
import com.geeksville.mesh.android.Logging
|
||||||
import com.geeksville.mesh.database.MeshLogRepository
|
import com.geeksville.mesh.database.MeshLogRepository
|
||||||
import com.geeksville.mesh.database.entity.MeshLog
|
import com.geeksville.mesh.database.entity.MeshLog
|
||||||
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
|
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
data class MetricsState(
|
data class MetricsState(
|
||||||
val isManaged: Boolean = true,
|
val isManaged: Boolean = true,
|
||||||
|
val isFahrenheit: Boolean = false,
|
||||||
val deviceMetrics: List<Telemetry> = emptyList(),
|
val deviceMetrics: List<Telemetry> = emptyList(),
|
||||||
val environmentMetrics: List<Telemetry> = emptyList(),
|
val environmentMetrics: List<Telemetry> = emptyList(),
|
||||||
val signalMetrics: List<MeshPacket> = emptyList(),
|
val signalMetrics: List<MeshPacket> = emptyList(),
|
||||||
val hasTracerouteLogs: Boolean = false,
|
val tracerouteRequests: List<MeshLog> = emptyList(),
|
||||||
val environmentDisplayFahrenheit: Boolean = false,
|
val tracerouteResults: List<MeshPacket> = emptyList(),
|
||||||
) {
|
) {
|
||||||
fun hasDeviceMetrics() = deviceMetrics.isNotEmpty()
|
fun hasDeviceMetrics() = deviceMetrics.isNotEmpty()
|
||||||
fun hasEnvironmentMetrics() = environmentMetrics.isNotEmpty()
|
fun hasEnvironmentMetrics() = environmentMetrics.isNotEmpty()
|
||||||
fun hasSignalMetrics() = signalMetrics.isNotEmpty()
|
fun hasSignalMetrics() = signalMetrics.isNotEmpty()
|
||||||
|
fun hasTracerouteLogs() = tracerouteRequests.isNotEmpty()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val Empty = MetricsState()
|
val Empty = MetricsState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class TracerouteLogState(
|
|
||||||
val requests: List<MeshLog> = emptyList(),
|
|
||||||
val results: List<MeshPacket> = emptyList(),
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
val Empty = TracerouteLogState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class MetricsViewModel @Inject constructor(
|
class MetricsViewModel @Inject constructor(
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
private val meshLogRepository: MeshLogRepository,
|
private val meshLogRepository: MeshLogRepository,
|
||||||
private val radioConfigRepository: RadioConfigRepository,
|
private val radioConfigRepository: RadioConfigRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel(), Logging {
|
||||||
private val destNum = MutableStateFlow(0)
|
private val destNum = MutableStateFlow(0)
|
||||||
|
|
||||||
private fun MeshPacket.hasValidSignal(): Boolean =
|
private fun MeshPacket.hasValidSignal(): Boolean =
|
||||||
|
@ -66,48 +62,65 @@ class MetricsViewModel @Inject constructor(
|
||||||
meshLogRepository.deleteLog(uuid)
|
meshLogRepository.deleteLog(uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
private val _state = MutableStateFlow(MetricsState.Empty)
|
||||||
val tracerouteState = destNum.flatMapLatest { destNum ->
|
val state: StateFlow<MetricsState> = _state
|
||||||
combine(
|
|
||||||
meshLogRepository.getLogsFrom(nodeNum = 0, PortNum.TRACEROUTE_APP_VALUE),
|
|
||||||
meshLogRepository.getMeshPacketsFrom(destNum, PortNum.TRACEROUTE_APP_VALUE),
|
|
||||||
) { request, response ->
|
|
||||||
TracerouteLogState(
|
|
||||||
requests = request.filter { it.hasValidTraceroute() },
|
|
||||||
results = response,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.stateIn(
|
|
||||||
scope = viewModelScope,
|
|
||||||
started = WhileSubscribed(stopTimeoutMillis = 5000L),
|
|
||||||
initialValue = TracerouteLogState.Empty,
|
|
||||||
)
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
init {
|
||||||
val state = destNum.flatMapLatest { destNum ->
|
radioConfigRepository.deviceProfileFlow.onEach { profile ->
|
||||||
combine(
|
|
||||||
meshLogRepository.getTelemetryFrom(destNum),
|
|
||||||
meshLogRepository.getMeshPacketsFrom(destNum),
|
|
||||||
meshLogRepository.getLogsFrom(nodeNum = 0, PortNum.TRACEROUTE_APP_VALUE),
|
|
||||||
radioConfigRepository.deviceProfileFlow,
|
|
||||||
) { telemetry, meshPackets, traceroute, profile ->
|
|
||||||
val moduleConfig = profile.moduleConfig
|
val moduleConfig = profile.moduleConfig
|
||||||
MetricsState(
|
_state.update { state ->
|
||||||
isManaged = profile.config.security.isManaged,
|
state.copy(
|
||||||
deviceMetrics = telemetry.filter { it.hasDeviceMetrics() },
|
isManaged = profile.config.security.isManaged,
|
||||||
environmentMetrics = telemetry.filter {
|
isFahrenheit = moduleConfig.telemetry.environmentDisplayFahrenheit,
|
||||||
it.hasEnvironmentMetrics() && it.environmentMetrics.relativeHumidity >= 0f
|
)
|
||||||
},
|
}
|
||||||
signalMetrics = meshPackets.filter { it.hasValidSignal() },
|
}.launchIn(viewModelScope)
|
||||||
hasTracerouteLogs = traceroute.any { it.hasValidTraceroute() },
|
|
||||||
environmentDisplayFahrenheit = moduleConfig.telemetry.environmentDisplayFahrenheit,
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
)
|
destNum.flatMapLatest { destNum ->
|
||||||
}
|
meshLogRepository.getTelemetryFrom(destNum).onEach { telemetry ->
|
||||||
}.stateIn(
|
_state.update { state ->
|
||||||
scope = viewModelScope,
|
state.copy(
|
||||||
started = WhileSubscribed(stopTimeoutMillis = 5000L),
|
deviceMetrics = telemetry.filter { it.hasDeviceMetrics() },
|
||||||
initialValue = MetricsState.Empty,
|
environmentMetrics = telemetry.filter {
|
||||||
)
|
it.hasEnvironmentMetrics() && it.environmentMetrics.relativeHumidity >= 0f
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.launchIn(viewModelScope)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
destNum.flatMapLatest { destNum ->
|
||||||
|
meshLogRepository.getMeshPacketsFrom(destNum).onEach { meshPackets ->
|
||||||
|
_state.update { state ->
|
||||||
|
state.copy(signalMetrics = meshPackets.filter { it.hasValidSignal() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.launchIn(viewModelScope)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
destNum.flatMapLatest { destNum ->
|
||||||
|
combine(
|
||||||
|
meshLogRepository.getLogsFrom(nodeNum = 0, PortNum.TRACEROUTE_APP_VALUE),
|
||||||
|
meshLogRepository.getMeshPacketsFrom(destNum, PortNum.TRACEROUTE_APP_VALUE),
|
||||||
|
) { request, response ->
|
||||||
|
_state.update { state ->
|
||||||
|
state.copy(
|
||||||
|
tracerouteRequests = request.filter { it.hasValidTraceroute() },
|
||||||
|
tracerouteResults = response,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.launchIn(viewModelScope)
|
||||||
|
|
||||||
|
debug("MetricsViewModel created")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
debug("MetricsViewModel cleared")
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to set the Node for which the user will see charts for.
|
* Used to set the Node for which the user will see charts for.
|
||||||
|
|
|
@ -120,7 +120,7 @@ private fun NodeDetailList(
|
||||||
if (node.hasEnvironmentMetrics) {
|
if (node.hasEnvironmentMetrics) {
|
||||||
item {
|
item {
|
||||||
PreferenceCategory("Environment")
|
PreferenceCategory("Environment")
|
||||||
EnvironmentMetrics(node, metricsState.environmentDisplayFahrenheit)
|
EnvironmentMetrics(node, metricsState.isFahrenheit)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -161,7 +161,7 @@ private fun NodeDetailList(
|
||||||
NavCard(
|
NavCard(
|
||||||
title = stringResource(R.string.traceroute_logs),
|
title = stringResource(R.string.traceroute_logs),
|
||||||
icon = Icons.Default.Route,
|
icon = Icons.Default.Route,
|
||||||
enabled = metricsState.hasTracerouteLogs
|
enabled = metricsState.hasTracerouteLogs()
|
||||||
) {
|
) {
|
||||||
onNavigate("TracerouteList")
|
onNavigate("TracerouteList")
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,7 +83,7 @@ fun EnvironmentMetricsScreen(
|
||||||
return (celsius * 1.8F) + 32
|
return (celsius * 1.8F) + 32
|
||||||
}
|
}
|
||||||
|
|
||||||
val processedTelemetries: List<Telemetry> = if (state.environmentDisplayFahrenheit) {
|
val processedTelemetries: List<Telemetry> = if (state.isFahrenheit) {
|
||||||
state.environmentMetrics.map { telemetry ->
|
state.environmentMetrics.map { telemetry ->
|
||||||
val temperatureFahrenheit =
|
val temperatureFahrenheit =
|
||||||
celsiusToFahrenheit(telemetry.environmentMetrics.temperature)
|
celsiusToFahrenheit(telemetry.environmentMetrics.temperature)
|
||||||
|
@ -124,7 +124,7 @@ fun EnvironmentMetricsScreen(
|
||||||
items(processedTelemetries) { telemetry ->
|
items(processedTelemetries) { telemetry ->
|
||||||
EnvironmentMetricsCard(
|
EnvironmentMetricsCard(
|
||||||
telemetry,
|
telemetry,
|
||||||
state.environmentDisplayFahrenheit
|
state.isFahrenheit
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ fun TracerouteLogScreen(
|
||||||
viewModel: MetricsViewModel = hiltViewModel(),
|
viewModel: MetricsViewModel = hiltViewModel(),
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val state by viewModel.tracerouteState.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
val dateFormat = remember {
|
val dateFormat = remember {
|
||||||
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
|
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
|
||||||
}
|
}
|
||||||
|
@ -80,9 +80,9 @@ fun TracerouteLogScreen(
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
) {
|
) {
|
||||||
items(state.requests, key = { it.uuid }) { log ->
|
items(state.tracerouteRequests, key = { it.uuid }) { log ->
|
||||||
val result = remember(state.requests) {
|
val result = remember(state.tracerouteRequests) {
|
||||||
state.results.find { it.decoded.requestId == log.fromRadio.packet.id }
|
state.tracerouteResults.find { it.decoded.requestId == log.fromRadio.packet.id }
|
||||||
}
|
}
|
||||||
val route = remember(result) { result?.fullRouteDiscovery }
|
val route = remember(result) { result?.fullRouteDiscovery }
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue