Skip to content

Commit

Permalink
refactor: split MetricsViewModel state updates
Browse files Browse the repository at this point in the history
- 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 coroutines
  • Loading branch information
andrekir committed Nov 2, 2024
1 parent dcd5ca1 commit 2ca5c46
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 63 deletions.
123 changes: 68 additions & 55 deletions app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,51 +6,47 @@ import com.geeksville.mesh.CoroutineDispatchers
import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.Portnums.PortNum
import com.geeksville.mesh.TelemetryProtos.Telemetry
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.database.MeshLogRepository
import com.geeksville.mesh.database.entity.MeshLog
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
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.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 javax.inject.Inject

data class MetricsState(
val isManaged: Boolean = true,
val isFahrenheit: Boolean = false,
val deviceMetrics: List<Telemetry> = emptyList(),
val environmentMetrics: List<Telemetry> = emptyList(),
val signalMetrics: List<MeshPacket> = emptyList(),
val hasTracerouteLogs: Boolean = false,
val environmentDisplayFahrenheit: Boolean = false,
val tracerouteRequests: List<MeshLog> = emptyList(),
val tracerouteResults: List<MeshPacket> = emptyList(),
) {
fun hasDeviceMetrics() = deviceMetrics.isNotEmpty()
fun hasEnvironmentMetrics() = environmentMetrics.isNotEmpty()
fun hasSignalMetrics() = signalMetrics.isNotEmpty()
fun hasTracerouteLogs() = tracerouteRequests.isNotEmpty()

companion object {
val Empty = MetricsState()
}
}

data class TracerouteLogState(
val requests: List<MeshLog> = emptyList(),
val results: List<MeshPacket> = emptyList(),
) {
companion object {
val Empty = TracerouteLogState()
}
}

@HiltViewModel
class MetricsViewModel @Inject constructor(
private val dispatchers: CoroutineDispatchers,
private val meshLogRepository: MeshLogRepository,
private val radioConfigRepository: RadioConfigRepository,
) : ViewModel() {
) : ViewModel(), Logging {
private val destNum = MutableStateFlow(0)

private fun MeshPacket.hasValidSignal(): Boolean =
Expand All @@ -66,48 +62,65 @@ class MetricsViewModel @Inject constructor(
meshLogRepository.deleteLog(uuid)
}

@OptIn(ExperimentalCoroutinesApi::class)
val tracerouteState = destNum.flatMapLatest { destNum ->
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)
val state = destNum.flatMapLatest { destNum ->
combine(
meshLogRepository.getTelemetryFrom(destNum),
meshLogRepository.getMeshPacketsFrom(destNum),
meshLogRepository.getLogsFrom(nodeNum = 0, PortNum.TRACEROUTE_APP_VALUE),
radioConfigRepository.deviceProfileFlow,
) { telemetry, meshPackets, traceroute, profile ->
private val _state = MutableStateFlow(MetricsState.Empty)
val state: StateFlow<MetricsState> = _state

init {
radioConfigRepository.deviceProfileFlow.onEach { profile ->
val moduleConfig = profile.moduleConfig
MetricsState(
isManaged = profile.config.security.isManaged,
deviceMetrics = telemetry.filter { it.hasDeviceMetrics() },
environmentMetrics = telemetry.filter {
it.hasEnvironmentMetrics() && it.environmentMetrics.relativeHumidity >= 0f
},
signalMetrics = meshPackets.filter { it.hasValidSignal() },
hasTracerouteLogs = traceroute.any { it.hasValidTraceroute() },
environmentDisplayFahrenheit = moduleConfig.telemetry.environmentDisplayFahrenheit,
)
}
}.stateIn(
scope = viewModelScope,
started = WhileSubscribed(stopTimeoutMillis = 5000L),
initialValue = MetricsState.Empty,
)
_state.update { state ->
state.copy(
isManaged = profile.config.security.isManaged,
isFahrenheit = moduleConfig.telemetry.environmentDisplayFahrenheit,
)
}
}.launchIn(viewModelScope)

@OptIn(ExperimentalCoroutinesApi::class)
destNum.flatMapLatest { destNum ->
meshLogRepository.getTelemetryFrom(destNum).onEach { telemetry ->
_state.update { state ->
state.copy(
deviceMetrics = telemetry.filter { it.hasDeviceMetrics() },
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.
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/com/geeksville/mesh/ui/NodeDetail.kt
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ private fun NodeDetailList(
if (node.hasEnvironmentMetrics) {
item {
PreferenceCategory("Environment")
EnvironmentMetrics(node, metricsState.environmentDisplayFahrenheit)
EnvironmentMetrics(node, metricsState.isFahrenheit)
Spacer(modifier = Modifier.height(8.dp))
}
}
Expand Down Expand Up @@ -161,7 +161,7 @@ private fun NodeDetailList(
NavCard(
title = stringResource(R.string.traceroute_logs),
icon = Icons.Default.Route,
enabled = metricsState.hasTracerouteLogs
enabled = metricsState.hasTracerouteLogs()
) {
onNavigate("TracerouteList")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ fun EnvironmentMetricsScreen(
return (celsius * 1.8F) + 32
}

val processedTelemetries: List<Telemetry> = if (state.environmentDisplayFahrenheit) {
val processedTelemetries: List<Telemetry> = if (state.isFahrenheit) {
state.environmentMetrics.map { telemetry ->
val temperatureFahrenheit =
celsiusToFahrenheit(telemetry.environmentMetrics.temperature)
Expand Down Expand Up @@ -124,7 +124,7 @@ fun EnvironmentMetricsScreen(
items(processedTelemetries) { telemetry ->
EnvironmentMetricsCard(
telemetry,
state.environmentDisplayFahrenheit
state.isFahrenheit
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ fun TracerouteLogScreen(
viewModel: MetricsViewModel = hiltViewModel(),
modifier: Modifier = Modifier,
) {
val state by viewModel.tracerouteState.collectAsStateWithLifecycle()
val state by viewModel.state.collectAsStateWithLifecycle()
val dateFormat = remember {
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
}
Expand All @@ -80,9 +80,9 @@ fun TracerouteLogScreen(
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp),
) {
items(state.requests, key = { it.uuid }) { log ->
val result = remember(state.requests) {
state.results.find { it.decoded.requestId == log.fromRadio.packet.id }
items(state.tracerouteRequests, key = { it.uuid }) { log ->
val result = remember(state.tracerouteRequests) {
state.tracerouteResults.find { it.decoded.requestId == log.fromRadio.packet.id }
}
val route = remember(result) { result?.fullRouteDiscovery }

Expand Down

0 comments on commit 2ca5c46

Please sign in to comment.