Skip to content

Commit

Permalink
feat: Metrics time selection (#1396)
Browse files Browse the repository at this point in the history
  • Loading branch information
Robert-0410 authored Nov 11, 2024
1 parent 5480174 commit 7e54ad9
Show file tree
Hide file tree
Showing 9 changed files with 526 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@ class MeshLogRepository @Inject constructor(private val meshLogDaoLazy: dagger.L
.toBuilder().setTime((log.received_date / MILLIS_TO_SECONDS).toInt()).build()
}.getOrNull()

/**
* @param timeFrame the oldest [Telemetry] to get.
*/
@OptIn(ExperimentalCoroutinesApi::class)
fun getTelemetryFrom(nodeNum: Int): Flow<List<Telemetry>> =
meshLogDao.getLogsFrom(nodeNum, Portnums.PortNum.TELEMETRY_APP_VALUE, MAX_MESH_PACKETS)
fun getTelemetryFrom(nodeNum: Int, timeFrame: Long): Flow<List<Telemetry>> =
meshLogDao.getLogsFrom(nodeNum, Portnums.PortNum.TELEMETRY_APP_VALUE, MAX_MESH_PACKETS, timeFrame)
.distinctUntilChanged()
.mapLatest { list -> list.mapNotNull(::parseTelemetryLog) }
.flowOn(Dispatchers.IO)
Expand All @@ -43,7 +46,8 @@ class MeshLogRepository @Inject constructor(private val meshLogDaoLazy: dagger.L
nodeNum: Int,
portNum: Int = Portnums.PortNum.UNKNOWN_APP_VALUE,
maxItem: Int = MAX_MESH_PACKETS,
): Flow<List<MeshLog>> = meshLogDao.getLogsFrom(nodeNum, portNum, maxItem)
oldestTime: Long = 0L
): Flow<List<MeshLog>> = meshLogDao.getLogsFrom(nodeNum, portNum, maxItem, oldestTime)
.distinctUntilChanged()
.flowOn(Dispatchers.IO)

Expand All @@ -55,7 +59,8 @@ class MeshLogRepository @Inject constructor(private val meshLogDaoLazy: dagger.L
fun getMeshPacketsFrom(
nodeNum: Int,
portNum: Int = Portnums.PortNum.UNKNOWN_APP_VALUE,
): Flow<List<MeshPacket>> = getLogsFrom(nodeNum, portNum)
oldestTime: Long = 0L
): Flow<List<MeshPacket>> = getLogsFrom(nodeNum, portNum, oldestTime = oldestTime)
.mapLatest { list -> list.map { it.fromRadio.packet } }
.flowOn(Dispatchers.IO)

Expand Down
12 changes: 7 additions & 5 deletions app/src/main/java/com/geeksville/mesh/database/dao/MeshLogDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,20 @@ interface MeshLogDao {
@Query("SELECT * FROM log ORDER BY received_date ASC LIMIT 0,:maxItem")
fun getAllLogsInReceiveOrder(maxItem: Int): Flow<List<MeshLog>>

/*
* Retrieves MeshPackets matching 'from_num' (nodeNum) and 'port_num' (PortNum).
* If 'portNum' is 0, returns all MeshPackets. Otherwise, filters by 'port_num'.
/**
* Retrieves [MeshLog]s matching 'from_num' (nodeNum) and 'port_num' (PortNum).
*
* @param portNum If 0, returns all MeshPackets. Otherwise, filters by 'port_num'.
* @param timeFrame oldest limit in milliseconds of [MeshLog]s we want to retrieve.
*/
@Query(
"""
SELECT * FROM log
WHERE from_num = :fromNum AND (:portNum = 0 AND port_num != 0 OR port_num = :portNum)
WHERE from_num = :fromNum AND (:portNum = 0 AND port_num != 0 OR port_num = :portNum) AND received_date > :timeFrame
ORDER BY received_date DESC LIMIT 0,:maxItem
"""
)
fun getLogsFrom(fromNum: Int, portNum: Int, maxItem: Int): Flow<List<MeshLog>>
fun getLogsFrom(fromNum: Int, portNum: Int, maxItem: Int, timeFrame: Long = 0L): Flow<List<MeshLog>>

@Insert
fun insert(log: MeshLog)
Expand Down
62 changes: 51 additions & 11 deletions app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.geeksville.mesh.model

import android.app.Application
import android.net.Uri
import androidx.annotation.StringRes
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
Expand All @@ -11,16 +12,19 @@ import com.geeksville.mesh.CoroutineDispatchers
import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.MeshProtos.Position
import com.geeksville.mesh.Portnums.PortNum
import com.geeksville.mesh.R
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 com.geeksville.mesh.ui.Route
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
Expand Down Expand Up @@ -55,6 +59,28 @@ data class MetricsState(
}
}

/**
* Supported time frames used to display data.
*/
@Suppress("MagicNumber")
enum class TimeFrame(
val milliseconds: Long,
@StringRes val strRes: Int
) {
TWENTY_FOUR_HOURS(86400000L, R.string.twenty_four_hours),
FORTY_EIGHT_HOURS(172800000L, R.string.forty_eight_hours),
ONE_WEEK(604800000L, R.string.one_week),
TWO_WEEKS(1209600000L, R.string.two_weeks),
ONE_MONTH(2629800000L, R.string.one_month),
MAX(0L, R.string.max);

fun calculateOldestTime(): Long = if (this == MAX) {
MAX.milliseconds
} else {
System.currentTimeMillis() - this.milliseconds
}
}

private fun MeshPacket.hasValidSignal(): Boolean =
rxTime > 0 && (rxSnr != 0f && rxRssi != 0) && (hopStart > 0 && hopStart - hopLimit == 0)

Expand Down Expand Up @@ -91,6 +117,9 @@ class MetricsViewModel @Inject constructor(
private val _state = MutableStateFlow(MetricsState.Empty)
val state: StateFlow<MetricsState> = _state

private val _timeFrame = MutableStateFlow(TimeFrame.TWENTY_FOUR_HOURS)
val timeFrame: StateFlow<TimeFrame> = _timeFrame

init {
radioConfigRepository.deviceProfileFlow.onEach { profile ->
val moduleConfig = profile.moduleConfig
Expand All @@ -102,20 +131,27 @@ class MetricsViewModel @Inject constructor(
}
}.launchIn(viewModelScope)

meshLogRepository.getTelemetryFrom(destNum).onEach { telemetry ->
_state.update { state ->
state.copy(
deviceMetrics = telemetry.filter { it.hasDeviceMetrics() },
environmentMetrics = telemetry.filter {
it.hasEnvironmentMetrics() && it.environmentMetrics.relativeHumidity >= 0f
},
)
@OptIn(ExperimentalCoroutinesApi::class)
_timeFrame.flatMapLatest { timeFrame ->
meshLogRepository.getTelemetryFrom(destNum, timeFrame.calculateOldestTime()).onEach { telemetry ->
_state.update { state ->
state.copy(
deviceMetrics = telemetry.filter { it.hasDeviceMetrics() },
environmentMetrics = telemetry.filter {
it.hasEnvironmentMetrics() && it.environmentMetrics.relativeHumidity >= 0f
},
)
}
}
}.launchIn(viewModelScope)

meshLogRepository.getMeshPacketsFrom(destNum).onEach { meshPackets ->
_state.update { state ->
state.copy(signalMetrics = meshPackets.filter { it.hasValidSignal() })
@OptIn(ExperimentalCoroutinesApi::class)
_timeFrame.flatMapLatest { timeFrame ->
val oldestTime = timeFrame.calculateOldestTime()
meshLogRepository.getMeshPacketsFrom(destNum, oldestTime = oldestTime).onEach { meshPackets ->
_state.update { state ->
state.copy(signalMetrics = meshPackets.filter { it.hasValidSignal() })
}
}
}.launchIn(viewModelScope)

Expand Down Expand Up @@ -143,6 +179,10 @@ class MetricsViewModel @Inject constructor(
debug("MetricsViewModel cleared")
}

fun setTimeFrame(timeFrame: TimeFrame) {
_timeFrame.value = timeFrame
}

/**
* Write the persisted Position data out to a CSV file in the specified location.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.geeksville.mesh.R
Expand Down Expand Up @@ -183,18 +184,20 @@ fun Legend(legendData: List<LegendData>, promptInfoDialog: () -> Unit) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
Spacer(modifier = Modifier.weight(1f))
for (data in legendData) {
legendData.forEachIndexed { index, data ->
LegendLabel(
text = stringResource(data.nameRes),
color = data.color,
isLine = data.isLine
)

Spacer(modifier = Modifier.width(4.dp))
if (index != legendData.lastIndex) {
Spacer(modifier = Modifier.weight(1f))
}
}
Spacer(modifier = Modifier.width(4.dp))

Icon(
imageVector = Icons.Default.Info,
Expand Down Expand Up @@ -276,3 +279,13 @@ private fun LegendLabel(text: String, color: Color, isLine: Boolean = false) {
fontSize = MaterialTheme.typography.button.fontSize,
)
}

@Preview
@Composable
private fun LegendPreview() {
val data = listOf(
LegendData(nameRes = R.string.rssi, color = Color.Red),
LegendData(nameRes = R.string.snr, color = Color.Green)
)
Legend(legendData = data, promptInfoDialog = {})
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
Expand Down Expand Up @@ -84,6 +85,15 @@ fun DeviceMetricsScreen(
state.deviceMetrics.reversed(),
promptInfoDialog = { displayInfoDialog = true }
)

val selectedTimeFrame by viewModel.timeFrame.collectAsState()
MetricsTimeSelector(
selectedTimeFrame,
onOptionSelected = { viewModel.setTimeFrame(it) }
) {
TimeLabel(stringResource(it.strRes))
}

/* Device Metric Cards */
LazyColumn(
modifier = Modifier.fillMaxSize()
Expand Down Expand Up @@ -156,7 +166,7 @@ private fun DeviceMetricsChart(
center = Offset(x1, yChUtil)
)

/* Air Utilization Transmit */
/* Air Utilization Transmit */
val airUtilRatio = telemetry.deviceMetrics.airUtilTx / MAX_PERCENT_VALUE
val yAirUtil = height - (airUtilRatio * height)
drawCircle(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
Expand Down Expand Up @@ -117,6 +118,14 @@ fun EnvironmentMetricsScreen(
promptInfoDialog = { displayInfoDialog = true }
)

val selectedTimeFrame by viewModel.timeFrame.collectAsState()
MetricsTimeSelector(
selectedTimeFrame,
onOptionSelected = { viewModel.setTimeFrame(it) }
) {
TimeLabel(stringResource(it.strRes))
}

/* Environment Metric Cards */
LazyColumn(
modifier = Modifier.fillMaxSize()
Expand Down
Loading

0 comments on commit 7e54ad9

Please sign in to comment.