diff --git a/core/strings/src/commonMain/composeResources/values/strings.xml b/core/strings/src/commonMain/composeResources/values/strings.xml
index 1356b4875..120aa7680 100644
--- a/core/strings/src/commonMain/composeResources/values/strings.xml
+++ b/core/strings/src/commonMain/composeResources/values/strings.xml
@@ -446,11 +446,13 @@
%1$s - %2$s
Route traced toward destination:\n\n
Route traced back to us:\n\n
+ 1H
24H
48H
1W
2W
4W
+ 1M
Max
Unknown Age
Copy
@@ -1175,4 +1177,5 @@
Bring your device close to the NFC tag to scan.
Generate QR Code
NFC is disabled. Please enable it in system settings.
+ All
diff --git a/feature/node/build.gradle.kts b/feature/node/build.gradle.kts
index 4c3e6e849..24b65d6a5 100644
--- a/feature/node/build.gradle.kts
+++ b/feature/node/build.gradle.kts
@@ -27,6 +27,8 @@ configure {
namespace = "org.meshtastic.feature.node"
defaultConfig { manifestPlaceholders["MAPS_API_KEY"] = "DEBUG_KEY" }
+
+ testOptions { unitTests { isIncludeAndroidResources = true } }
}
dependencies {
@@ -66,4 +68,5 @@ dependencies {
testImplementation(libs.androidx.compose.ui.test.junit4)
testImplementation(libs.androidx.test.ext.junit)
testImplementation(libs.robolectric)
+ debugImplementation(libs.androidx.compose.ui.test.manifest)
}
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/BaseMetricChart.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/BaseMetricChart.kt
index b6d6d0bc7..275a0a1eb 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/BaseMetricChart.kt
+++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/BaseMetricChart.kt
@@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Info
@@ -32,16 +33,14 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.meshtastic.core.strings.getString
import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost
import com.patrykandpatrick.vico.compose.cartesian.Scroll
import com.patrykandpatrick.vico.compose.cartesian.VicoScrollState
@@ -66,7 +65,6 @@ import org.meshtastic.core.strings.logs
import org.meshtastic.core.ui.component.MainAppBar
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.Refresh
-import org.meshtastic.feature.node.detail.NodeRequestEffect
/**
* A generic chart host for Meshtastic metric charts. Handles common boilerplate for markers, scrolling, and point
@@ -147,18 +145,19 @@ fun AdaptiveMetricLayout(
@Composable
@Suppress("LongMethod")
fun BaseMetricScreen(
- viewModel: MetricsViewModel,
onNavigateUp: () -> Unit,
telemetryType: TelemetryType?,
titleRes: StringResource,
+ nodeName: String,
data: List,
timeProvider: (T) -> Double,
infoData: List = emptyList(),
+ snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
+ onRequestTelemetry: (() -> Unit)? = null,
chartPart: @Composable (Modifier, Double?, VicoScrollState, (Double) -> Unit) -> Unit,
- listPart: @Composable (Modifier, Double?, (Double) -> Unit) -> Unit,
+ listPart: @Composable (Modifier, Double?, LazyListState, (Double) -> Unit) -> Unit,
+ controlPart: @Composable () -> Unit = {},
) {
- val state by viewModel.state.collectAsStateWithLifecycle()
- val snackbarHostState = remember { SnackbarHostState() }
var displayInfoDialog by remember { mutableStateOf(false) }
val lazyListState = rememberLazyListState()
@@ -166,21 +165,10 @@ fun BaseMetricScreen(
val coroutineScope = rememberCoroutineScope()
var selectedX by remember { mutableStateOf(null) }
- LaunchedEffect(Unit) {
- viewModel.effects.collect { effect ->
- when (effect) {
- is NodeRequestEffect.ShowFeedback -> {
- @Suppress("SpreadOperator")
- snackbarHostState.showSnackbar(getString(effect.resource, *effect.args.toTypedArray()))
- }
- }
- }
- }
-
Scaffold(
topBar = {
MainAppBar(
- title = state.node?.user?.long_name ?: "",
+ title = nodeName,
subtitle = stringResource(titleRes) + " (${data.size} ${stringResource(Res.string.logs)})",
ourNode = null,
showNodeChip = false,
@@ -193,7 +181,10 @@ fun BaseMetricScreen(
}
}
if (telemetryType != null) {
- IconButton(onClick = { viewModel.requestTelemetry(telemetryType) }) {
+ IconButton(
+ onClick = { onRequestTelemetry?.invoke() },
+ modifier = Modifier.testTag("refresh_button"),
+ ) {
Icon(imageVector = MeshtasticIcons.Refresh, contentDescription = null)
}
}
@@ -208,6 +199,8 @@ fun BaseMetricScreen(
LegendInfoDialog(infoData = infoData, onDismiss = { displayInfoDialog = false })
}
+ controlPart()
+
AdaptiveMetricLayout(
chartPart = { modifier ->
chartPart(modifier, selectedX, vicoScrollState) { x ->
@@ -219,7 +212,7 @@ fun BaseMetricScreen(
}
},
listPart = { modifier ->
- listPart(modifier, selectedX) { x ->
+ listPart(modifier, selectedX, lazyListState) { x ->
selectedX = x
coroutineScope.launch {
vicoScrollState.animateScroll(Scroll.Absolute.x(x, CommonCharts.SCROLL_BIAS))
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/CommonCharts.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/CommonCharts.kt
index 8c486bb92..1e782d39d 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/CommonCharts.kt
+++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/CommonCharts.kt
@@ -76,7 +76,6 @@ object CommonCharts {
val DATE_TIME_FORMAT: DateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM)
val TIME_MINUTE_FORMAT: DateFormat = DateFormat.getTimeInstance(DateFormat.SHORT)
val TIME_SECONDS_FORMAT: DateFormat = DateFormat.getTimeInstance(DateFormat.MEDIUM)
- val DATE_TIME_MINUTE_FORMAT: DateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT)
val DATE_FORMAT: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT)
const val MS_PER_SEC = 1000L
const val MAX_PERCENT_VALUE = 100f
@@ -105,14 +104,17 @@ object CommonCharts {
val zoom = if (context is CartesianDrawingContext) context.zoom else 1f
val visibleSpan = xLength / zoom
- val formatter =
- when {
- visibleSpan <= 3600 -> TIME_SECONDS_FORMAT // < 1 hour visible
- visibleSpan <= 86400 * 2 -> TIME_MINUTE_FORMAT // < 2 days visible
- visibleSpan <= 86400 * 14 -> DATE_TIME_MINUTE_FORMAT // < 2 weeks visible
- else -> DATE_FORMAT
+ when {
+ visibleSpan <= 3600 -> TIME_SECONDS_FORMAT.format(date) // < 1 hour visible
+ visibleSpan <= 86400 * 2 -> TIME_MINUTE_FORMAT.format(date) // < 2 days visible
+ visibleSpan <= 86400 * 14 -> {
+ // < 2 weeks visible: separate date and time with a newline
+ val dateStr = DATE_FORMAT.format(date)
+ val timeStr = TIME_MINUTE_FORMAT.format(date)
+ "$dateStr\n$timeStr"
}
- formatter.format(date)
+ else -> DATE_FORMAT.format(date)
+ }
}
}
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt
index 7fc17e597..8f5d5723f 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt
+++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.kt
@@ -37,6 +37,7 @@ import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -52,6 +53,7 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.meshtastic.core.strings.getString
import com.patrykandpatrick.vico.compose.cartesian.VicoScrollState
import com.patrykandpatrick.vico.compose.cartesian.axis.Axis
import com.patrykandpatrick.vico.compose.cartesian.axis.HorizontalAxis
@@ -79,6 +81,7 @@ import org.meshtastic.core.ui.theme.GraphColors.Cyan
import org.meshtastic.core.ui.theme.GraphColors.Gold
import org.meshtastic.core.ui.theme.GraphColors.Green
import org.meshtastic.core.ui.theme.GraphColors.Purple
+import org.meshtastic.feature.node.detail.NodeRequestEffect
import org.meshtastic.feature.node.metrics.CommonCharts.DATE_TIME_FORMAT
import org.meshtastic.feature.node.metrics.CommonCharts.MS_PER_SEC
import org.meshtastic.proto.Telemetry
@@ -122,13 +125,27 @@ private val LEGEND_DATA =
@Composable
fun DeviceMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -> Unit) {
val state by viewModel.state.collectAsStateWithLifecycle()
- val data = state.deviceMetrics
+ val timeFrame by viewModel.timeFrame.collectAsStateWithLifecycle()
+ val availableTimeFrames by viewModel.availableTimeFrames.collectAsStateWithLifecycle()
+ val data = state.deviceMetrics.filter { (it.time ?: 0).toLong() >= timeFrame.timeThreshold() }
+ val snackbarHostState = remember { SnackbarHostState() }
val hasBattery = remember(data) { data.any { it.device_metrics?.battery_level != null } }
val hasVoltage = remember(data) { data.any { it.device_metrics?.voltage != null } }
val hasChUtil = remember(data) { data.any { it.device_metrics?.channel_utilization != null } }
val hasAirUtil = remember(data) { data.any { it.device_metrics?.air_util_tx != null } }
+ LaunchedEffect(Unit) {
+ viewModel.effects.collect { effect ->
+ when (effect) {
+ is NodeRequestEffect.ShowFeedback -> {
+ @Suppress("SpreadOperator")
+ snackbarHostState.showSnackbar(getString(effect.resource, *effect.args.toTypedArray()))
+ }
+ }
+ }
+ }
+
val filteredLegendData =
remember(hasBattery, hasVoltage, hasChUtil, hasAirUtil) {
LEGEND_DATA.filter { d ->
@@ -167,13 +184,23 @@ fun DeviceMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigat
}
BaseMetricScreen(
- viewModel = viewModel,
onNavigateUp = onNavigateUp,
telemetryType = TelemetryType.DEVICE,
titleRes = Res.string.device_metrics_log,
+ nodeName = state.node?.user?.long_name ?: "",
data = data,
timeProvider = { (it.time ?: 0).toDouble() },
infoData = infoItems,
+ snackbarHostState = snackbarHostState,
+ onRequestTelemetry = { viewModel.requestTelemetry(TelemetryType.DEVICE) },
+ controlPart = {
+ TimeFrameSelector(
+ selectedTimeFrame = timeFrame,
+ availableTimeFrames = availableTimeFrames,
+ onTimeFrameSelected = viewModel::setTimeFrame,
+ modifier = Modifier.padding(horizontal = 16.dp),
+ )
+ },
chartPart = { modifier, selectedX, vicoScrollState, onPointSelected ->
DeviceMetricsChart(
modifier = modifier,
@@ -184,8 +211,8 @@ fun DeviceMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigat
onPointSelected = onPointSelected,
)
},
- listPart = { modifier, selectedX, onCardClick ->
- LazyColumn(modifier = modifier.fillMaxSize()) {
+ listPart = { modifier, selectedX, lazyListState, onCardClick ->
+ LazyColumn(modifier = modifier.fillMaxSize(), state = lazyListState) {
itemsIndexed(data) { _, telemetry ->
DeviceMetricsCard(
telemetry = telemetry,
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt
index 750b3159d..f1eacf50f 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt
+++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.kt
@@ -29,28 +29,18 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
-import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.selection.SelectionContainer
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.Info
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -59,12 +49,8 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.meshtastic.core.strings.getString
-import com.patrykandpatrick.vico.compose.cartesian.Scroll
-import com.patrykandpatrick.vico.compose.cartesian.rememberVicoScrollState
-import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.TelemetryType
-import org.meshtastic.core.model.util.UnitConversions.celsiusToFahrenheit
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.current
import org.meshtastic.core.strings.env_metrics_log
@@ -72,8 +58,6 @@ import org.meshtastic.core.strings.gas_resistance
import org.meshtastic.core.strings.humidity
import org.meshtastic.core.strings.iaq
import org.meshtastic.core.strings.iaq_definition
-import org.meshtastic.core.strings.info
-import org.meshtastic.core.strings.logs
import org.meshtastic.core.strings.lux
import org.meshtastic.core.strings.radiation
import org.meshtastic.core.strings.soil_moisture
@@ -83,28 +67,19 @@ import org.meshtastic.core.strings.uv_lux
import org.meshtastic.core.strings.voltage
import org.meshtastic.core.ui.component.IaqDisplayMode
import org.meshtastic.core.ui.component.IndoorAirQuality
-import org.meshtastic.core.ui.component.MainAppBar
-import org.meshtastic.core.ui.icon.MeshtasticIcons
-import org.meshtastic.core.ui.icon.Refresh
import org.meshtastic.feature.node.detail.NodeRequestEffect
import org.meshtastic.feature.node.metrics.CommonCharts.DATE_TIME_FORMAT
import org.meshtastic.feature.node.metrics.CommonCharts.MS_PER_SEC
-import org.meshtastic.feature.node.metrics.CommonCharts.SCROLL_BIAS
import org.meshtastic.proto.Telemetry
-@Suppress("LongMethod")
@Composable
fun EnvironmentMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNavigateUp: () -> Unit) {
val state by viewModel.state.collectAsStateWithLifecycle()
- val environmentState by viewModel.environmentState.collectAsStateWithLifecycle()
+ val graphData by viewModel.environmentGraphingData.collectAsStateWithLifecycle()
+ val filteredTelemetries by viewModel.filteredEnvironmentMetrics.collectAsStateWithLifecycle()
+ val timeFrame by viewModel.timeFrame.collectAsStateWithLifecycle()
+ val availableTimeFrames by viewModel.availableTimeFrames.collectAsStateWithLifecycle()
val snackbarHostState = remember { SnackbarHostState() }
- val graphData = environmentState.environmentMetricsForGraphing(state.isFahrenheit)
- val data = graphData.metrics
-
- val lazyListState = rememberLazyListState()
- val vicoScrollState = rememberVicoScrollState()
- val coroutineScope = rememberCoroutineScope()
- var selectedX by remember { mutableStateOf(null) }
LaunchedEffect(Unit) {
viewModel.effects.collect { effect ->
@@ -117,99 +92,47 @@ fun EnvironmentMetricsScreen(viewModel: MetricsViewModel = hiltViewModel(), onNa
}
}
- val processedTelemetries: List =
- if (state.isFahrenheit) {
- data.map { telemetry ->
- val em = telemetry.environment_metrics ?: return@map telemetry
- val temperatureFahrenheit = em.temperature?.let { celsiusToFahrenheit(it) }
- val soilTemperatureFahrenheit = em.soil_temperature?.let { celsiusToFahrenheit(it) }
- telemetry.copy(
- environment_metrics =
- em.copy(temperature = temperatureFahrenheit, soil_temperature = soilTemperatureFahrenheit),
- )
- }
- } else {
- data
- }
-
- var displayInfoDialog by remember { mutableStateOf(false) }
-
- Scaffold(
- topBar = {
- MainAppBar(
- title = state.node?.user?.long_name ?: "",
- subtitle =
- stringResource(Res.string.env_metrics_log) +
- " (${processedTelemetries.size} ${stringResource(Res.string.logs)})",
- ourNode = null,
- showNodeChip = false,
- canNavigateUp = true,
- onNavigateUp = onNavigateUp,
- actions = {
- IconButton(onClick = { displayInfoDialog = true }) {
- Icon(imageVector = Icons.Rounded.Info, contentDescription = stringResource(Res.string.info))
- }
- if (!state.isLocal) {
- IconButton(onClick = { viewModel.requestTelemetry(TelemetryType.ENVIRONMENT) }) {
- androidx.compose.material3.Icon(
- imageVector = MeshtasticIcons.Refresh,
- contentDescription = null,
- )
- }
- }
- },
- onClickChip = {},
+ BaseMetricScreen(
+ onNavigateUp = onNavigateUp,
+ telemetryType = TelemetryType.ENVIRONMENT,
+ titleRes = Res.string.env_metrics_log,
+ nodeName = state.node?.user?.long_name ?: "",
+ data = filteredTelemetries,
+ timeProvider = { (it.time ?: 0).toDouble() },
+ infoData = listOf(InfoDialogData(Res.string.iaq, Res.string.iaq_definition, Environment.IAQ.color)),
+ snackbarHostState = snackbarHostState,
+ onRequestTelemetry = { viewModel.requestTelemetry(TelemetryType.ENVIRONMENT) },
+ controlPart = {
+ TimeFrameSelector(
+ selectedTimeFrame = timeFrame,
+ availableTimeFrames = availableTimeFrames,
+ onTimeFrameSelected = viewModel::setTimeFrame,
+ modifier = Modifier.padding(horizontal = 16.dp),
)
},
- snackbarHost = { SnackbarHost(snackbarHostState) },
- ) { innerPadding ->
- Column(modifier = Modifier.padding(innerPadding)) {
- if (displayInfoDialog) {
- LegendInfoDialog(
- infoData = listOf(InfoDialogData(Res.string.iaq, Res.string.iaq_definition, Environment.IAQ.color)),
- onDismiss = { displayInfoDialog = false },
- )
- }
-
- AdaptiveMetricLayout(
- chartPart = { modifier ->
- EnvironmentMetricsChart(
- modifier = modifier,
- telemetries = processedTelemetries.reversed(),
- graphData = graphData,
- vicoScrollState = vicoScrollState,
- selectedX = selectedX,
- onPointSelected = { x ->
- selectedX = x
- val index = processedTelemetries.indexOfFirst { (it.time ?: 0).toDouble() == x }
- if (index != -1) {
- coroutineScope.launch { lazyListState.animateScrollToItem(index) }
- }
- },
- )
- },
- listPart = { modifier ->
- LazyColumn(modifier = modifier.fillMaxSize(), state = lazyListState) {
- itemsIndexed(processedTelemetries) { _, telemetry ->
- EnvironmentMetricsCard(
- telemetry = telemetry,
- environmentDisplayFahrenheit = state.isFahrenheit,
- isSelected = (telemetry.time ?: 0).toDouble() == selectedX,
- onClick = {
- selectedX = (telemetry.time ?: 0).toDouble()
- coroutineScope.launch {
- vicoScrollState.animateScroll(
- Scroll.Absolute.x((telemetry.time ?: 0).toDouble(), SCROLL_BIAS),
- )
- }
- },
- )
- }
- }
- },
+ chartPart = { modifier, selectedX, vicoScrollState, onPointSelected ->
+ EnvironmentMetricsChart(
+ modifier = modifier,
+ telemetries = filteredTelemetries.reversed(),
+ graphData = graphData,
+ vicoScrollState = vicoScrollState,
+ selectedX = selectedX,
+ onPointSelected = onPointSelected,
)
- }
- }
+ },
+ listPart = { modifier, selectedX, lazyListState, onCardClick ->
+ LazyColumn(modifier = modifier.fillMaxSize(), state = lazyListState) {
+ itemsIndexed(filteredTelemetries) { _, telemetry ->
+ EnvironmentMetricsCard(
+ telemetry = telemetry,
+ environmentDisplayFahrenheit = state.isFahrenheit,
+ isSelected = (telemetry.time ?: 0).toDouble() == selectedX,
+ onClick = { onCardClick((telemetry.time ?: 0).toDouble()) },
+ )
+ }
+ }
+ },
+ )
}
@Composable
@@ -374,9 +297,9 @@ private fun VoltageCurrentDisplay(envMetrics: org.meshtastic.proto.EnvironmentMe
)
}
if (hasCurrent) {
- val current = envMetrics.current!!
+ val currentValue = envMetrics.current!!
Text(
- text = "%s %.2f mA".format(stringResource(Res.string.current), current),
+ text = "%s %.2f mA".format(stringResource(Res.string.current), currentValue),
color = MaterialTheme.colorScheme.onSurface,
fontSize = MaterialTheme.typography.labelLarge.fontSize,
)
@@ -443,8 +366,6 @@ private fun EnvironmentMetricsCard(
isSelected: Boolean,
onClick: () -> Unit,
) {
- val envMetrics = telemetry.environment_metrics ?: org.meshtastic.proto.EnvironmentMetrics()
- val time = (telemetry.time ?: 0).toLong() * MS_PER_SEC
Card(
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp).clickable { onClick() },
border = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null,
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsState.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsState.kt
index 4333ffc30..0e042d9e7 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsState.kt
+++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetricsState.kt
@@ -41,7 +41,8 @@ enum class Environment(val color: Color) {
override fun getValue(telemetry: Telemetry) = telemetry.environment_metrics?.soil_temperature
},
SOIL_MOISTURE(Purple) {
- override fun getValue(telemetry: Telemetry) = telemetry.environment_metrics?.soil_moisture?.toFloat()
+ override fun getValue(telemetry: Telemetry) =
+ telemetry.environment_metrics?.soil_moisture?.takeIf { it != Int.MIN_VALUE }?.toFloat()
},
BAROMETRIC_PRESSURE(Green) {
override fun getValue(telemetry: Telemetry) = telemetry.environment_metrics?.barometric_pressure
@@ -50,7 +51,8 @@ enum class Environment(val color: Color) {
override fun getValue(telemetry: Telemetry) = telemetry.environment_metrics?.gas_resistance
},
IAQ(Cyan) {
- override fun getValue(telemetry: Telemetry) = telemetry.environment_metrics?.iaq?.toFloat()
+ override fun getValue(telemetry: Telemetry) =
+ telemetry.environment_metrics?.iaq?.takeIf { it != Int.MIN_VALUE }?.toFloat()
},
LUX(Gold) {
override fun getValue(telemetry: Telemetry) = telemetry.environment_metrics?.lux
@@ -136,7 +138,8 @@ data class EnvironmentMetricsState(val environmentMetrics: List = emp
}
// Soil Moisture
- val soilMoistures = telemetries.mapNotNull { it.environment_metrics?.soil_moisture?.takeIf { it != 0 } }
+ val soilMoistures =
+ telemetries.mapNotNull { it.environment_metrics?.soil_moisture?.takeIf { it != Int.MIN_VALUE } }
if (soilMoistures.isNotEmpty()) {
minValues.add(soilMoistures.minOf { it.toFloat() })
maxValues.add(soilMoistures.maxOf { it.toFloat() })
@@ -144,7 +147,7 @@ data class EnvironmentMetricsState(val environmentMetrics: List = emp
}
// IAQ
- val iaqs = telemetries.mapNotNull { it.environment_metrics?.iaq?.takeIf { it != 0 } }
+ val iaqs = telemetries.mapNotNull { it.environment_metrics?.iaq?.takeIf { it != Int.MIN_VALUE } }
if (iaqs.isNotEmpty()) {
minValues.add(iaqs.minOf { it.toFloat() })
maxValues.add(iaqs.maxOf { it.toFloat() })
diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt
index 2fe570184..b04692af7 100644
--- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt
+++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt
@@ -54,10 +54,9 @@ import org.meshtastic.core.database.model.Node
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.TelemetryType
-import org.meshtastic.core.model.TracerouteMapAvailability
import org.meshtastic.core.model.evaluateTracerouteMapAvailability
+import org.meshtastic.core.model.util.UnitConversions
import org.meshtastic.core.navigation.NodesRoutes
-import org.meshtastic.core.service.ServiceAction
import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.fallback_node_name
@@ -72,23 +71,30 @@ import org.meshtastic.feature.map.model.TracerouteOverlay
import org.meshtastic.feature.node.detail.NodeRequestActions
import org.meshtastic.feature.node.detail.NodeRequestEffect
import org.meshtastic.feature.node.model.MetricsState
+import org.meshtastic.feature.node.model.TimeFrame
import org.meshtastic.proto.Config
import org.meshtastic.proto.HardwareModel
import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.PortNum
+import org.meshtastic.proto.Telemetry
import org.meshtastic.proto.User
import java.io.BufferedWriter
import java.io.FileNotFoundException
import java.io.FileWriter
+import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Locale
import javax.inject.Inject
+import org.meshtastic.proto.Paxcount as ProtoPaxcount
private const val DEFAULT_ID_SUFFIX_LENGTH = 4
-private fun MeshPacket.hasValidSignal(): Boolean = (rx_time ?: 0) > 0 &&
- ((rx_snr ?: 0f) != 0f && (rx_rssi ?: 0) != 0) &&
- ((hop_start ?: 0) > 0 && (hop_start ?: 0) - (hop_limit ?: 0) == 0)
+private fun MeshPacket.hasValidSignal(): Boolean = (rx_time ?: 0) > 0 && ((rx_snr ?: 0f) != 0f || (rx_rssi ?: 0) != 0)
+
+private fun Telemetry.hasValidEnvironmentMetrics(): Boolean {
+ val metrics = this.environment_metrics ?: return false
+ return metrics.relative_humidity != null && metrics.temperature != null && metrics.temperature?.isNaN() != true
+}
@Suppress("LongParameterList", "TooManyFunctions")
@HiltViewModel
@@ -115,11 +121,11 @@ constructor(
private val tracerouteOverlayCache = MutableStateFlow