Meshtastic-Android/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt

118 lines
4.4 KiB
Kotlin

package com.geeksville.mesh.model
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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.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.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
data class MetricsState(
val isManaged: Boolean = true,
val deviceMetrics: List<Telemetry> = emptyList(),
val environmentMetrics: List<Telemetry> = emptyList(),
val signalMetrics: List<MeshPacket> = emptyList(),
val hasTracerouteLogs: Boolean = false,
val environmentDisplayFahrenheit: Boolean = false,
) {
fun hasDeviceMetrics() = deviceMetrics.isNotEmpty()
fun hasEnvironmentMetrics() = environmentMetrics.isNotEmpty()
fun hasSignalMetrics() = signalMetrics.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() {
private val destNum = MutableStateFlow(0)
private fun MeshPacket.hasValidSignal(): Boolean =
rxTime > 0 && (rxSnr != 0f && rxRssi != 0) && (hopStart > 0 && hopStart - hopLimit == 0)
private fun MeshLog.hasValidTraceroute(): Boolean = with(fromRadio.packet) {
hasDecoded() && decoded.wantResponse && from == 0 && to == destNum.value
}
fun getUser(nodeNum: Int) = radioConfigRepository.getUser(nodeNum)
fun deleteLog(uuid: String) = viewModelScope.launch(dispatchers.io) {
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 ->
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,
)
/**
* Used to set the Node for which the user will see charts for.
*/
fun setSelectedNode(nodeNum: Int) {
destNum.value = nodeNum
}
}