kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
refactor: Conducting time filter at the component lvl to avoid metric nav cards from being disabled when we don't have recent data (#1402)
rodzic
f6af9b8782
commit
013e3de792
|
@ -32,12 +32,9 @@ class MeshLogRepository @Inject constructor(private val meshLogDaoLazy: dagger.L
|
||||||
.toBuilder().setTime((log.received_date / MILLIS_TO_SECONDS).toInt()).build()
|
.toBuilder().setTime((log.received_date / MILLIS_TO_SECONDS).toInt()).build()
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
|
|
||||||
/**
|
|
||||||
* @param timeFrame the oldest [Telemetry] to get.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun getTelemetryFrom(nodeNum: Int, timeFrame: Long): Flow<List<Telemetry>> =
|
fun getTelemetryFrom(nodeNum: Int): Flow<List<Telemetry>> =
|
||||||
meshLogDao.getLogsFrom(nodeNum, Portnums.PortNum.TELEMETRY_APP_VALUE, MAX_MESH_PACKETS, timeFrame)
|
meshLogDao.getLogsFrom(nodeNum, Portnums.PortNum.TELEMETRY_APP_VALUE, MAX_MESH_PACKETS)
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.mapLatest { list -> list.mapNotNull(::parseTelemetryLog) }
|
.mapLatest { list -> list.mapNotNull(::parseTelemetryLog) }
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
|
@ -46,8 +43,7 @@ class MeshLogRepository @Inject constructor(private val meshLogDaoLazy: dagger.L
|
||||||
nodeNum: Int,
|
nodeNum: Int,
|
||||||
portNum: Int = Portnums.PortNum.UNKNOWN_APP_VALUE,
|
portNum: Int = Portnums.PortNum.UNKNOWN_APP_VALUE,
|
||||||
maxItem: Int = MAX_MESH_PACKETS,
|
maxItem: Int = MAX_MESH_PACKETS,
|
||||||
oldestTime: Long = 0L
|
): Flow<List<MeshLog>> = meshLogDao.getLogsFrom(nodeNum, portNum, maxItem)
|
||||||
): Flow<List<MeshLog>> = meshLogDao.getLogsFrom(nodeNum, portNum, maxItem, oldestTime)
|
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
|
|
||||||
|
@ -59,8 +55,7 @@ class MeshLogRepository @Inject constructor(private val meshLogDaoLazy: dagger.L
|
||||||
fun getMeshPacketsFrom(
|
fun getMeshPacketsFrom(
|
||||||
nodeNum: Int,
|
nodeNum: Int,
|
||||||
portNum: Int = Portnums.PortNum.UNKNOWN_APP_VALUE,
|
portNum: Int = Portnums.PortNum.UNKNOWN_APP_VALUE,
|
||||||
oldestTime: Long = 0L
|
): Flow<List<MeshPacket>> = getLogsFrom(nodeNum, portNum)
|
||||||
): Flow<List<MeshPacket>> = getLogsFrom(nodeNum, portNum, oldestTime = oldestTime)
|
|
||||||
.mapLatest { list -> list.map { it.fromRadio.packet } }
|
.mapLatest { list -> list.map { it.fromRadio.packet } }
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
|
|
||||||
|
|
|
@ -19,16 +19,15 @@ interface MeshLogDao {
|
||||||
* Retrieves [MeshLog]s matching 'from_num' (nodeNum) and 'port_num' (PortNum).
|
* Retrieves [MeshLog]s matching 'from_num' (nodeNum) and 'port_num' (PortNum).
|
||||||
*
|
*
|
||||||
* @param portNum If 0, returns all MeshPackets. Otherwise, filters by 'port_num'.
|
* @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(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT * FROM log
|
SELECT * FROM log
|
||||||
WHERE from_num = :fromNum AND (:portNum = 0 AND port_num != 0 OR port_num = :portNum) AND received_date > :timeFrame
|
WHERE from_num = :fromNum AND (:portNum = 0 AND port_num != 0 OR port_num = :portNum)
|
||||||
ORDER BY received_date DESC LIMIT 0,:maxItem
|
ORDER BY received_date DESC LIMIT 0,:maxItem
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
fun getLogsFrom(fromNum: Int, portNum: Int, maxItem: Int, timeFrame: Long = 0L): Flow<List<MeshLog>>
|
fun getLogsFrom(fromNum: Int, portNum: Int, maxItem: Int): Flow<List<MeshLog>>
|
||||||
|
|
||||||
@Insert
|
@Insert
|
||||||
fun insert(log: MeshLog)
|
fun insert(log: MeshLog)
|
||||||
|
|
|
@ -20,11 +20,9 @@ import com.geeksville.mesh.database.entity.MeshLog
|
||||||
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
|
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
|
||||||
import com.geeksville.mesh.ui.Route
|
import com.geeksville.mesh.ui.Route
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
@ -35,6 +33,7 @@ import java.io.FileNotFoundException
|
||||||
import java.io.FileWriter
|
import java.io.FileWriter
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
data class MetricsState(
|
data class MetricsState(
|
||||||
|
@ -54,6 +53,21 @@ data class MetricsState(
|
||||||
fun hasTracerouteLogs() = tracerouteRequests.isNotEmpty()
|
fun hasTracerouteLogs() = tracerouteRequests.isNotEmpty()
|
||||||
fun hasPositionLogs() = positionLogs.isNotEmpty()
|
fun hasPositionLogs() = positionLogs.isNotEmpty()
|
||||||
|
|
||||||
|
fun deviceMetricsFiltered(timeFrame: TimeFrame): List<Telemetry> {
|
||||||
|
val oldestTime = timeFrame.calculateOldestTime()
|
||||||
|
return deviceMetrics.filter { it.time >= oldestTime }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun environmentMetricsFiltered(timeFrame: TimeFrame): List<Telemetry> {
|
||||||
|
val oldestTime = timeFrame.calculateOldestTime()
|
||||||
|
return environmentMetrics.filter { it.time >= oldestTime }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun signalMetricsFiltered(timeFrame: TimeFrame): List<MeshPacket> {
|
||||||
|
val oldestTime = timeFrame.calculateOldestTime()
|
||||||
|
return signalMetrics.filter { it.rxTime >= oldestTime }
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val Empty = MetricsState()
|
val Empty = MetricsState()
|
||||||
}
|
}
|
||||||
|
@ -64,20 +78,20 @@ data class MetricsState(
|
||||||
*/
|
*/
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
enum class TimeFrame(
|
enum class TimeFrame(
|
||||||
val milliseconds: Long,
|
val seconds: Long,
|
||||||
@StringRes val strRes: Int
|
@StringRes val strRes: Int
|
||||||
) {
|
) {
|
||||||
TWENTY_FOUR_HOURS(86400000L, R.string.twenty_four_hours),
|
TWENTY_FOUR_HOURS(TimeUnit.DAYS.toSeconds(1), R.string.twenty_four_hours),
|
||||||
FORTY_EIGHT_HOURS(172800000L, R.string.forty_eight_hours),
|
FORTY_EIGHT_HOURS(TimeUnit.DAYS.toSeconds(2), R.string.forty_eight_hours),
|
||||||
ONE_WEEK(604800000L, R.string.one_week),
|
ONE_WEEK(TimeUnit.DAYS.toSeconds(7), R.string.one_week),
|
||||||
TWO_WEEKS(1209600000L, R.string.two_weeks),
|
TWO_WEEKS(TimeUnit.DAYS.toSeconds(14), R.string.two_weeks),
|
||||||
ONE_MONTH(2629800000L, R.string.one_month),
|
FOUR_WEEKS(TimeUnit.DAYS.toSeconds(28), R.string.four_weeks),
|
||||||
MAX(0L, R.string.max);
|
MAX(0L, R.string.max);
|
||||||
|
|
||||||
fun calculateOldestTime(): Long = if (this == MAX) {
|
fun calculateOldestTime(): Long = if (this == MAX) {
|
||||||
MAX.milliseconds
|
MAX.seconds
|
||||||
} else {
|
} else {
|
||||||
System.currentTimeMillis() - this.milliseconds
|
System.currentTimeMillis() / 1000 - this.seconds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,27 +145,20 @@ class MetricsViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}.launchIn(viewModelScope)
|
}.launchIn(viewModelScope)
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
meshLogRepository.getTelemetryFrom(destNum).onEach { telemetry ->
|
||||||
_timeFrame.flatMapLatest { timeFrame ->
|
_state.update { state ->
|
||||||
meshLogRepository.getTelemetryFrom(destNum, timeFrame.calculateOldestTime()).onEach { telemetry ->
|
state.copy(
|
||||||
_state.update { state ->
|
deviceMetrics = telemetry.filter { it.hasDeviceMetrics() },
|
||||||
state.copy(
|
environmentMetrics = telemetry.filter {
|
||||||
deviceMetrics = telemetry.filter { it.hasDeviceMetrics() },
|
it.hasEnvironmentMetrics() && it.environmentMetrics.relativeHumidity >= 0f
|
||||||
environmentMetrics = telemetry.filter {
|
}
|
||||||
it.hasEnvironmentMetrics() && it.environmentMetrics.relativeHumidity >= 0f
|
)
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}.launchIn(viewModelScope)
|
}.launchIn(viewModelScope)
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
meshLogRepository.getMeshPacketsFrom(destNum).onEach { meshPackets ->
|
||||||
_timeFrame.flatMapLatest { timeFrame ->
|
_state.update { state ->
|
||||||
val oldestTime = timeFrame.calculateOldestTime()
|
state.copy(signalMetrics = meshPackets.filter { it.hasValidSignal() })
|
||||||
meshLogRepository.getMeshPacketsFrom(destNum, oldestTime = oldestTime).onEach { meshPackets ->
|
|
||||||
_state.update { state ->
|
|
||||||
state.copy(signalMetrics = meshPackets.filter { it.hasValidSignal() })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}.launchIn(viewModelScope)
|
}.launchIn(viewModelScope)
|
||||||
|
|
||||||
|
|
|
@ -65,6 +65,8 @@ fun DeviceMetricsScreen(
|
||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
var displayInfoDialog by remember { mutableStateOf(false) }
|
var displayInfoDialog by remember { mutableStateOf(false) }
|
||||||
|
val selectedTimeFrame by viewModel.timeFrame.collectAsState()
|
||||||
|
val data = state.deviceMetricsFiltered(selectedTimeFrame)
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
|
|
||||||
|
@ -82,11 +84,10 @@ fun DeviceMetricsScreen(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.fillMaxHeight(fraction = 0.33f),
|
.fillMaxHeight(fraction = 0.33f),
|
||||||
state.deviceMetrics.reversed(),
|
data.reversed(),
|
||||||
promptInfoDialog = { displayInfoDialog = true }
|
promptInfoDialog = { displayInfoDialog = true }
|
||||||
)
|
)
|
||||||
|
|
||||||
val selectedTimeFrame by viewModel.timeFrame.collectAsState()
|
|
||||||
MetricsTimeSelector(
|
MetricsTimeSelector(
|
||||||
selectedTimeFrame,
|
selectedTimeFrame,
|
||||||
onOptionSelected = { viewModel.setTimeFrame(it) }
|
onOptionSelected = { viewModel.setTimeFrame(it) }
|
||||||
|
@ -98,7 +99,7 @@ fun DeviceMetricsScreen(
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
items(state.deviceMetrics) { telemetry -> DeviceMetricsCard(telemetry) }
|
items(data) { telemetry -> DeviceMetricsCard(telemetry) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,6 +77,8 @@ fun EnvironmentMetricsScreen(
|
||||||
viewModel: MetricsViewModel = hiltViewModel(),
|
viewModel: MetricsViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
|
val selectedTimeFrame by viewModel.timeFrame.collectAsState()
|
||||||
|
val data = state.environmentMetricsFiltered(selectedTimeFrame)
|
||||||
|
|
||||||
/* Convert Celsius to Fahrenheit */
|
/* Convert Celsius to Fahrenheit */
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
|
@ -85,7 +87,7 @@ fun EnvironmentMetricsScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
val processedTelemetries: List<Telemetry> = if (state.isFahrenheit) {
|
val processedTelemetries: List<Telemetry> = if (state.isFahrenheit) {
|
||||||
state.environmentMetrics.map { telemetry ->
|
data.map { telemetry ->
|
||||||
val temperatureFahrenheit =
|
val temperatureFahrenheit =
|
||||||
celsiusToFahrenheit(telemetry.environmentMetrics.temperature)
|
celsiusToFahrenheit(telemetry.environmentMetrics.temperature)
|
||||||
telemetry.copy {
|
telemetry.copy {
|
||||||
|
@ -94,7 +96,7 @@ fun EnvironmentMetricsScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
state.environmentMetrics
|
data
|
||||||
}
|
}
|
||||||
|
|
||||||
var displayInfoDialog by remember { mutableStateOf(false) }
|
var displayInfoDialog by remember { mutableStateOf(false) }
|
||||||
|
@ -118,7 +120,6 @@ fun EnvironmentMetricsScreen(
|
||||||
promptInfoDialog = { displayInfoDialog = true }
|
promptInfoDialog = { displayInfoDialog = true }
|
||||||
)
|
)
|
||||||
|
|
||||||
val selectedTimeFrame by viewModel.timeFrame.collectAsState()
|
|
||||||
MetricsTimeSelector(
|
MetricsTimeSelector(
|
||||||
selectedTimeFrame,
|
selectedTimeFrame,
|
||||||
onOptionSelected = { viewModel.setTimeFrame(it) }
|
onOptionSelected = { viewModel.setTimeFrame(it) }
|
||||||
|
|
|
@ -71,6 +71,8 @@ fun SignalMetricsScreen(
|
||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
var displayInfoDialog by remember { mutableStateOf(false) }
|
var displayInfoDialog by remember { mutableStateOf(false) }
|
||||||
|
val selectedTimeFrame by viewModel.timeFrame.collectAsState()
|
||||||
|
val data = state.signalMetricsFiltered(selectedTimeFrame)
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
|
|
||||||
|
@ -88,11 +90,10 @@ fun SignalMetricsScreen(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.fillMaxHeight(fraction = 0.33f),
|
.fillMaxHeight(fraction = 0.33f),
|
||||||
meshPackets = state.signalMetrics.reversed(),
|
meshPackets = data.reversed(),
|
||||||
promptInfoDialog = { displayInfoDialog = true }
|
promptInfoDialog = { displayInfoDialog = true }
|
||||||
)
|
)
|
||||||
|
|
||||||
val selectedTimeFrame by viewModel.timeFrame.collectAsState()
|
|
||||||
MetricsTimeSelector(
|
MetricsTimeSelector(
|
||||||
selectedTimeFrame,
|
selectedTimeFrame,
|
||||||
onOptionSelected = { viewModel.setTimeFrame(it) }
|
onOptionSelected = { viewModel.setTimeFrame(it) }
|
||||||
|
@ -103,7 +104,7 @@ fun SignalMetricsScreen(
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
items(state.signalMetrics) { meshPacket -> SignalMetricsCard(meshPacket) }
|
items(data) { meshPacket -> SignalMetricsCard(meshPacket) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -307,7 +307,7 @@
|
||||||
<string name="forty_eight_hours">48H</string>
|
<string name="forty_eight_hours">48H</string>
|
||||||
<string name="one_week">1W</string>
|
<string name="one_week">1W</string>
|
||||||
<string name="two_weeks">2W</string>
|
<string name="two_weeks">2W</string>
|
||||||
<string name="one_month">1M</string>
|
<string name="four_weeks">4W</string>
|
||||||
<string name="max">Max</string>
|
<string name="max">Max</string>
|
||||||
<string name="selected">Selected</string>
|
<string name="selected">Selected</string>
|
||||||
<string name="not_selected">Not Selected</string>
|
<string name="not_selected">Not Selected</string>
|
||||||
|
|
Ładowanie…
Reference in New Issue