Add soil temperature and soil moisture environmental metrics to app (#2419)

Co-authored-by: DaneEvans <dane@goneepic.com>
This commit is contained in:
Justin E. Mann 2025-07-12 07:52:06 -06:00 committed by GitHub
parent 8a0ad26d4e
commit fbd62cbf10
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 158 additions and 14 deletions

View file

@ -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,

View file

@ -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,

View file

@ -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<Float>()
val maxValues = mutableListOf<Float>()
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 }

View file

@ -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,

View file

@ -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)

View file

@ -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 */

View file

@ -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,

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="200dp" android:viewportHeight="32" android:viewportWidth="32" android:width="200dp">
<path android:fillColor="#000000" android:pathData="M24.5,30a5.202,5.202 0,0 1,-4.626 -8.08L23.49,16.538a1.217,1.217 0,0 1,2.02 0L29.06,21.815A5.492,5.492 0,0 1,30 24.751,5.385 5.385,0 0,1 24.5,30ZM24.5,18.62 L21.564,22.987A3.208,3.208 0,0 0,24.5 28,3.385 3.385,0 0,0 28,24.751a3.435,3.435 0,0 0,-0.63 -1.867Z"/>
<path android:fillColor="#000000" android:pathData="M11,16V11h1a4.004,4.004 0,0 0,4 -4V4H13a3.978,3.978 0,0 0,-2.747 1.107A6.003,6.003 0,0 0,5 2H2V5a6.007,6.007 0,0 0,6 6H9v5H2v2H16V16ZM13,6h1V7a2.002,2.002 0,0 1,-2 2H11V8A2.002,2.002 0,0 1,13 6ZM8,9A4.004,4.004 0,0 1,4 5V4H5A4.004,4.004 0,0 1,9 8V9Z"/>
<path android:fillColor="#000000" android:pathData="M2,21h14v2h-14z"/>
<path android:fillColor="#000000" android:pathData="M2,26h14v2h-14z"/>
</vector>

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="200dp" android:viewportHeight="32" android:viewportWidth="32" android:width="200dp">
<path android:fillColor="#000000" android:pathData="M11,16V11h1a4.004,4.004 0,0 0,4 -4V4H13a3.978,3.978 0,0 0,-2.747 1.107A6.003,6.003 0,0 0,5 2H2V5a6.007,6.007 0,0 0,6 6H9v5H2v2H16V16ZM13,6h1V7a2.002,2.002 0,0 1,-2 2H11V8A2.002,2.002 0,0 1,13 6ZM8,9A4.004,4.004 0,0 1,4 5V4H5A4.004,4.004 0,0 1,9 8V9Z"/>
<path android:fillColor="#000000" android:pathData="M2,21h14v2h-14z"/>
<path android:fillColor="#000000" android:pathData="M2,26h14v2h-14z"/>
<path android:fillColor="#000000" android:pathData="M25,30a4.986,4.986 0,0 1,-3 -8.98L22,15a3,3 0,0 1,6 0v6.02A4.986,4.986 0,0 1,25 30ZM25,14a1.001,1.001 0,0 0,-1 1v7.13l-0.497,0.289A2.968,2.968 0,0 0,22 25a3,3 0,0 0,6 0,2.968 2.968,0 0,0 -1.503,-2.581L26,22.13L26,15A1.001,1.001 0,0 0,25 14Z"/>
</vector>

View file

@ -310,6 +310,8 @@
<string name="air_utilization">Air Utilization</string>
<string name="temperature">Temperature</string>
<string name="humidity">Humidity</string>
<string name="soil_temperature">Soil Temperature</string>
<string name="soil_moisture">Soil Moisture</string>
<string name="logs">Logs</string>
<string name="hops_away">Hops Away</string>
<string name="hops_away_template">Hops Away: %1$d</string>