From fbd62cbf1086a362e3747edb01db984d06e68a83 Mon Sep 17 00:00:00 2001 From: "Justin E. Mann" Date: Sat, 12 Jul 2025 07:52:06 -0600 Subject: [PATCH] Add soil temperature and soil moisture environmental metrics to app (#2419) Co-authored-by: DaneEvans --- .../main/java/com/geeksville/mesh/NodeInfo.kt | 2 + .../mesh/database/entity/NodeEntity.kt | 2 + .../mesh/model/EnvironmentMetricsState.kt | 41 +++++++++++- .../java/com/geeksville/mesh/model/Node.kt | 20 +++++- .../geeksville/mesh/ui/common/theme/Color.kt | 2 + .../mesh/ui/metrics/EnvironmentMetrics.kt | 67 ++++++++++++++++--- .../com/geeksville/mesh/ui/node/NodeDetail.kt | 14 ++++ app/src/main/res/drawable/soil_moisture.xml | 11 +++ .../main/res/drawable/soil_temperature.xml | 11 +++ app/src/main/res/values/strings.xml | 2 + 10 files changed, 158 insertions(+), 14 deletions(-) create mode 100644 app/src/main/res/drawable/soil_moisture.xml create mode 100644 app/src/main/res/drawable/soil_temperature.xml diff --git a/app/src/main/java/com/geeksville/mesh/NodeInfo.kt b/app/src/main/java/com/geeksville/mesh/NodeInfo.kt index bf8bcbfdb..9108aa542 100644 --- a/app/src/main/java/com/geeksville/mesh/NodeInfo.kt +++ b/app/src/main/java/com/geeksville/mesh/NodeInfo.kt @@ -160,6 +160,8 @@ data class EnvironmentMetrics( val time: Int = currentTime(), // default to current time in secs (NOT MILLISECONDS!) val temperature: Float, val relativeHumidity: Float, + val soilTemperature: Float, + val soilMoisture: Int, val barometricPressure: Float, val gasResistance: Float, val voltage: Float, diff --git a/app/src/main/java/com/geeksville/mesh/database/entity/NodeEntity.kt b/app/src/main/java/com/geeksville/mesh/database/entity/NodeEntity.kt index e7b9c800d..53b3dd96a 100644 --- a/app/src/main/java/com/geeksville/mesh/database/entity/NodeEntity.kt +++ b/app/src/main/java/com/geeksville/mesh/database/entity/NodeEntity.kt @@ -215,6 +215,8 @@ data class NodeEntity( time = environmentTelemetry.time, temperature = environmentMetrics.temperature, relativeHumidity = environmentMetrics.relativeHumidity, + soilTemperature = environmentMetrics.soilTemperature, + soilMoisture = environmentMetrics.soilMoisture, barometricPressure = environmentMetrics.barometricPressure, gasResistance = environmentMetrics.gasResistance, voltage = environmentMetrics.voltage, diff --git a/app/src/main/java/com/geeksville/mesh/model/EnvironmentMetricsState.kt b/app/src/main/java/com/geeksville/mesh/model/EnvironmentMetricsState.kt index da9131598..632592071 100644 --- a/app/src/main/java/com/geeksville/mesh/model/EnvironmentMetricsState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/EnvironmentMetricsState.kt @@ -21,6 +21,8 @@ import androidx.compose.ui.graphics.Color import com.geeksville.mesh.TelemetryProtos.Telemetry import com.geeksville.mesh.ui.common.theme.InfantryBlue import com.geeksville.mesh.ui.common.theme.Orange +import com.geeksville.mesh.ui.common.theme.Pink +import com.geeksville.mesh.ui.common.theme.Purple import com.geeksville.mesh.util.UnitConversions enum class Environment(val color: Color) { @@ -34,6 +36,16 @@ enum class Environment(val color: Color) { return telemetry.environmentMetrics.relativeHumidity } }, + SOIL_TEMPERATURE(Pink) { + override fun getValue(telemetry: Telemetry): Float { + return telemetry.environmentMetrics.soilTemperature + } + }, + SOIL_MOISTURE(Purple) { + override fun getValue(telemetry: Telemetry): Float { + return telemetry.environmentMetrics.soilMoisture.toFloat() + } + }, IAQ(Color.Green) { override fun getValue(telemetry: Telemetry): Float { return telemetry.environmentMetrics.iaq.toFloat() @@ -75,7 +87,7 @@ data class EnvironmentMetricsState( * @param timeFrame used to filter * @return [EnvironmentGraphingData] */ - @Suppress("LongMethod") + @Suppress("LongMethod", "CyclomaticComplexMethod", "MagicNumber") fun environmentMetricsFiltered(timeFrame: TimeFrame, useFahrenheit: Boolean = false): EnvironmentGraphingData { val oldestTime = timeFrame.calculateOldestTime() val telemetries = environmentMetrics.filter { it.time >= oldestTime } @@ -84,7 +96,7 @@ data class EnvironmentMetricsState( return EnvironmentGraphingData(metrics = telemetries, shouldPlot = shouldPlot.toList()) } - /* Grab the combined min and max for temp, humidity, and iaq. */ + /* Grab the combined min and max for temp, humidity, soil_Temperature, soilMoisture and iaq. */ val minValues = mutableListOf() val maxValues = mutableListOf() val (minTemp, maxTemp) = Pair( @@ -114,6 +126,31 @@ data class EnvironmentMetricsState( shouldPlot[Environment.HUMIDITY.ordinal] = true } + var minSoilTemperatureValue = minTemp.environmentMetrics.soilTemperature + var maxSoilTemperatureValue = maxTemp.environmentMetrics.soilTemperature + if (useFahrenheit) { + minSoilTemperatureValue = UnitConversions.celsiusToFahrenheit(minSoilTemperatureValue) + maxSoilTemperatureValue = UnitConversions.celsiusToFahrenheit(maxSoilTemperatureValue) + } + if (minTemp.environmentMetrics.soilTemperature != 0f || + maxTemp.environmentMetrics.soilTemperature != 0f) { + minValues.add(minSoilTemperatureValue) + maxValues.add(maxSoilTemperatureValue) + shouldPlot[Environment.SOIL_TEMPERATURE.ordinal] = true + } + + val (minSoilMoisture, maxSoilMoisture) = Pair( + telemetries.minBy { it.environmentMetrics.soilMoisture }, + telemetries.maxBy { it.environmentMetrics.soilMoisture } + ) + val soilMoistureRange = 0..100 + if (minSoilMoisture.environmentMetrics.soilMoisture in soilMoistureRange || + maxSoilMoisture.environmentMetrics.soilMoisture in soilMoistureRange) { + minValues.add(minSoilMoisture.environmentMetrics.soilMoisture.toFloat()) + maxValues.add(maxSoilMoisture.environmentMetrics.soilMoisture.toFloat()) + shouldPlot[Environment.SOIL_MOISTURE.ordinal] = true + } + val (minIAQ, maxIAQ) = Pair( telemetries.minBy { it.environmentMetrics.iaq }, telemetries.maxBy { it.environmentMetrics.iaq } diff --git a/app/src/main/java/com/geeksville/mesh/model/Node.kt b/app/src/main/java/com/geeksville/mesh/model/Node.kt index 1b631e90a..7d8bec233 100644 --- a/app/src/main/java/com/geeksville/mesh/model/Node.kt +++ b/app/src/main/java/com/geeksville/mesh/model/Node.kt @@ -27,6 +27,7 @@ import com.geeksville.mesh.TelemetryProtos.EnvironmentMetrics import com.geeksville.mesh.TelemetryProtos.PowerMetrics import com.geeksville.mesh.database.entity.NodeEntity import com.geeksville.mesh.util.GPSFormat +import com.geeksville.mesh.util.UnitConversions.celsiusToFahrenheit import com.geeksville.mesh.util.latLongToMeter import com.geeksville.mesh.util.toDistanceString import com.google.protobuf.ByteString @@ -113,8 +114,7 @@ data class Node( private fun EnvironmentMetrics.getDisplayString(isFahrenheit: Boolean): String { val temp = if (temperature != 0f) { if (isFahrenheit) { - val fahrenheit = temperature * 1.8F + 32 - "%.1f°F".format(fahrenheit) + "%.1f°F".format(celsiusToFahrenheit(temperature)) } else { "%.1f°C".format(temperature) } @@ -122,6 +122,20 @@ data class Node( null } val humidity = if (relativeHumidity != 0f) "%.0f%%".format(relativeHumidity) else null + val soilTemperatureStr = if (soilTemperature != 0f) { + if (isFahrenheit) { + "%.1f°F".format(celsiusToFahrenheit(temperature)) + } else { + "%.1f°C".format(soilTemperature) + } + } else { + null + } + val soilMoistureRange = 0..100 + val soilMoisture = + if (soilMoisture in soilMoistureRange && soilTemperature != 0f) { + "%d%%".format(soilMoisture) + } else { null } val voltage = if (this.voltage != 0f) "%.2fV".format(this.voltage) else null val current = if (current != 0f) "%.1fmA".format(current) else null val iaq = if (iaq != 0) "IAQ: $iaq" else null @@ -129,6 +143,8 @@ data class Node( return listOfNotNull( temp, humidity, + soilTemperatureStr, + soilMoisture, voltage, current, iaq, diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/theme/Color.kt b/app/src/main/java/com/geeksville/mesh/ui/common/theme/Color.kt index 06279486c..561fcfb62 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/common/theme/Color.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/common/theme/Color.kt @@ -28,6 +28,8 @@ val Green = Color(0xFF30C047) val HyperlinkBlue = Color(0xFF43C3B0) val InfantryBlue = Color(red = 75, green = 119, blue = 190) +val Purple = Color(0xFF9C27B0) +val Pink = Color(red = 255, green = 102, blue = 204) val primaryLight = Color(0xFF306A42) val onPrimaryLight = Color(0xFFFFFFFF) diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt index 96c71d8d8..c7c2abbcf 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/EnvironmentMetrics.kt @@ -68,6 +68,8 @@ import com.geeksville.mesh.ui.common.components.IaqDisplayMode import com.geeksville.mesh.ui.common.components.IndoorAirQuality import com.geeksville.mesh.ui.common.components.OptionLabel import com.geeksville.mesh.ui.common.components.SlidingSelector +import com.geeksville.mesh.ui.common.theme.Pink +import com.geeksville.mesh.ui.common.theme.Purple import com.geeksville.mesh.ui.metrics.CommonCharts.DATE_TIME_FORMAT import com.geeksville.mesh.ui.metrics.CommonCharts.MS_PER_SEC import com.geeksville.mesh.util.GraphUtil.createPath @@ -78,6 +80,8 @@ import com.geeksville.mesh.util.UnitConversions.celsiusToFahrenheit private enum class Environment(val color: Color) { TEMPERATURE(Color.Red), RELATIVE_HUMIDITY(Color.Blue), + SOIL_TEMPERATURE(Pink), + SOIL_MOISTURE(Purple), BAROMETRIC_PRESSURE(Color.Green), GAS_RESISTANCE(Color.Yellow), IAQ(Color.Magenta) @@ -112,6 +116,18 @@ private val LEGEND_DATA_2 = listOf( isLine = true ) ) +private val LEGEND_DATA_3 = listOf( + LegendData( + nameRes = R.string.soil_temperature, + color = Environment.SOIL_TEMPERATURE.color, + isLine = true + ), + LegendData( + nameRes = R.string.soil_moisture, + color = Environment.SOIL_MOISTURE.color, + isLine = true + ), +) @Composable fun EnvironmentMetricsScreen( @@ -127,9 +143,13 @@ fun EnvironmentMetricsScreen( data.map { telemetry -> val temperatureFahrenheit = celsiusToFahrenheit(telemetry.environmentMetrics.temperature) + val soilTemperatureFahrenheit = + celsiusToFahrenheit(telemetry.environmentMetrics.soilTemperature) telemetry.copy { - environmentMetrics = - telemetry.environmentMetrics.copy { temperature = temperatureFahrenheit } + environmentMetrics = telemetry.environmentMetrics.copy { + temperature = temperatureFahrenheit } + environmentMetrics = telemetry.environmentMetrics.copy { + soilTemperature = soilTemperatureFahrenheit } } } } else { @@ -137,9 +157,7 @@ fun EnvironmentMetricsScreen( } var displayInfoDialog by remember { mutableStateOf(false) } - Column { - if (displayInfoDialog) { LegendInfoDialog( pairedRes = listOf( @@ -167,15 +185,11 @@ fun EnvironmentMetricsScreen( OptionLabel(stringResource(it.strRes)) } - /* Environment Metric Cards */ LazyColumn( modifier = Modifier.fillMaxSize() ) { items(processedTelemetries) { telemetry -> - EnvironmentMetricsCard( - telemetry, - state.isFahrenheit - ) + EnvironmentMetricsCard(telemetry, state.isFahrenheit) } } } @@ -320,12 +334,13 @@ private fun EnvironmentMetricsChart( Spacer(modifier = Modifier.height(16.dp)) Legend(LEGEND_DATA_1, displayInfoIcon = false) + Legend(LEGEND_DATA_3, displayInfoIcon = false) Legend(LEGEND_DATA_2, promptInfoDialog = promptInfoDialog) Spacer(modifier = Modifier.height(16.dp)) } -@Suppress("LongMethod") +@Suppress("LongMethod", "MagicNumber") @Composable private fun EnvironmentMetricsCard(telemetry: Telemetry, environmentDisplayFahrenheit: Boolean) { val envMetrics = telemetry.environmentMetrics @@ -387,6 +402,38 @@ private fun EnvironmentMetricsCard(telemetry: Telemetry, environmentDisplayFahre ) } } + + /* Soil Moisture and Soil Temperature */ + val soilMoistureRange = 0..100 + if (telemetry.environmentMetrics.hasSoilTemperature() || + telemetry.environmentMetrics.soilMoisture in soilMoistureRange) { + Spacer(modifier = Modifier.height(4.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + val soilTemperatureTextFormat = + if (environmentDisplayFahrenheit) "%s %.1f°F" else "%s %.1f°C" + val soilMoistureTextFormat = "%s %d%%" + Text( + text = soilMoistureTextFormat.format( + stringResource(R.string.soil_moisture), + envMetrics.soilMoisture + ), + color = MaterialTheme.colorScheme.onSurface, + fontSize = MaterialTheme.typography.labelLarge.fontSize + ) + Text( + text = soilTemperatureTextFormat.format( + stringResource(R.string.soil_temperature), + envMetrics.soilTemperature + ), + color = MaterialTheme.colorScheme.onSurface, + fontSize = MaterialTheme.typography.labelLarge.fontSize + ) + } + } + if (telemetry.environmentMetrics.hasIaq()) { Spacer(modifier = Modifier.height(4.dp)) /* Air Quality */ diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt b/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt index ea4150614..853979113 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt @@ -762,6 +762,20 @@ private fun EnvironmentMetrics( value = dewPoint.toTempString(isFahrenheit) ) } + if (hasSoilTemperature()) { + InfoCard( + icon = ImageVector.vectorResource(R.drawable.soil_temperature), + text = stringResource(R.string.soil_temperature), + value = soilTemperature.toTempString(isFahrenheit) + ) + } + if (hasSoilMoisture()) { + InfoCard( + icon = ImageVector.vectorResource(R.drawable.soil_moisture), + text = stringResource(R.string.soil_moisture), + value = "%d%%".format(soilMoisture) + ) + } if (hasBarometricPressure()) { InfoCard( icon = Icons.Default.Speed, diff --git a/app/src/main/res/drawable/soil_moisture.xml b/app/src/main/res/drawable/soil_moisture.xml new file mode 100644 index 000000000..cee547ca5 --- /dev/null +++ b/app/src/main/res/drawable/soil_moisture.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/soil_temperature.xml b/app/src/main/res/drawable/soil_temperature.xml new file mode 100644 index 000000000..6b1e4611f --- /dev/null +++ b/app/src/main/res/drawable/soil_temperature.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e0ac6796f..50f1ec672 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -310,6 +310,8 @@ Air Utilization Temperature Humidity + Soil Temperature + Soil Moisture Logs Hops Away Hops Away: %1$d