mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat(environment): add 1-Wire multi-thermometer (DS18B20) display support (#5130)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
099aea2d81
commit
f48fc61729
8 changed files with 162 additions and 7 deletions
|
|
@ -748,6 +748,7 @@
|
|||
<string name="rainfall_24h">Rain (24h)</string>
|
||||
<string name="weight">Weight</string>
|
||||
<string name="radiation">Radiation</string>
|
||||
<string name="one_wire_temperature">1-Wire Temp</string>
|
||||
<string name="store_forward_config"><![CDATA[Store & Forward Config]]></string>
|
||||
<string name="indoor_air_quality_iaq">Indoor Air Quality (IAQ)</string>
|
||||
<string name="url">URL</string>
|
||||
|
|
|
|||
|
|
@ -60,6 +60,10 @@ object GraphColors {
|
|||
val Lime = Color(0xFFCDDC39)
|
||||
val Indigo = Color(0xFF3F51B5)
|
||||
val DeepOrange = Color(0xFFFF5722)
|
||||
val Magenta = Color(0xFFE040FB)
|
||||
val SkyBlue = Color(0xFF03A9F4)
|
||||
val Chartreuse = Color(0xFF76FF03)
|
||||
val Coral = Color(0xFFFF6E40)
|
||||
}
|
||||
|
||||
object StatusColors {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import org.meshtastic.core.resources.ic_radioactive
|
|||
import org.meshtastic.core.resources.ic_soil_moisture
|
||||
import org.meshtastic.core.resources.ic_soil_temperature
|
||||
import org.meshtastic.core.resources.lux
|
||||
import org.meshtastic.core.resources.one_wire_temperature
|
||||
import org.meshtastic.core.resources.pressure
|
||||
import org.meshtastic.core.resources.radiation
|
||||
import org.meshtastic.core.resources.soil_moisture
|
||||
|
|
@ -222,6 +223,18 @@ internal fun EnvironmentMetrics(
|
|||
),
|
||||
)
|
||||
}
|
||||
// 1-Wire temperature sensors (up to 8 channels)
|
||||
one_wire_temperature
|
||||
.filterNot { it.isNaN() }
|
||||
.forEachIndexed { idx, temp ->
|
||||
add(
|
||||
DrawableMetricInfo(
|
||||
label = Res.string.one_wire_temperature,
|
||||
value = "${idx + 1}: ${temp.toTempString(isFahrenheit)}",
|
||||
icon = Res.drawable.ic_soil_temperature,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
FlowRow(
|
||||
|
|
|
|||
|
|
@ -127,6 +127,8 @@ data class LegendData(
|
|||
val color: Color,
|
||||
val isLine: Boolean = false,
|
||||
val metricKey: Any? = null,
|
||||
/** When non-null, overrides the resolved [nameRes] string in the legend label. */
|
||||
val labelOverride: String? = null,
|
||||
)
|
||||
|
||||
data class InfoDialogData(val titleRes: StringResource, val definitionRes: StringResource, val color: Color)
|
||||
|
|
@ -153,11 +155,12 @@ fun Legend(
|
|||
) {
|
||||
legendData.forEachIndexed { index, data ->
|
||||
val isVisible = index !in hiddenSet
|
||||
val label = data.labelOverride ?: stringResource(data.nameRes)
|
||||
if (onToggle != null) {
|
||||
FilterChip(
|
||||
selected = isVisible,
|
||||
onClick = { onToggle(index) },
|
||||
label = { Text(text = stringResource(data.nameRes), style = MaterialTheme.typography.labelSmall) },
|
||||
label = { Text(text = label, style = MaterialTheme.typography.labelSmall) },
|
||||
leadingIcon = { LegendIndicator(color = data.color, isLine = data.isLine) },
|
||||
modifier = Modifier.padding(horizontal = 2.dp),
|
||||
)
|
||||
|
|
@ -166,7 +169,7 @@ fun Legend(
|
|||
LegendIndicator(color = data.color, isLine = data.isLine)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = stringResource(data.nameRes),
|
||||
text = label,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontSize = MaterialTheme.typography.labelSmall.fontSize,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import org.meshtastic.core.resources.baro_pressure
|
|||
import org.meshtastic.core.resources.humidity
|
||||
import org.meshtastic.core.resources.iaq
|
||||
import org.meshtastic.core.resources.lux
|
||||
import org.meshtastic.core.resources.one_wire_temperature
|
||||
import org.meshtastic.core.resources.radiation
|
||||
import org.meshtastic.core.resources.soil_moisture
|
||||
import org.meshtastic.core.resources.soil_temperature
|
||||
|
|
@ -112,6 +113,27 @@ private val LEGEND_DATA_3 =
|
|||
),
|
||||
)
|
||||
|
||||
private val LEGEND_DATA_4 =
|
||||
listOf(
|
||||
Environment.ONE_WIRE_TEMP_1,
|
||||
Environment.ONE_WIRE_TEMP_2,
|
||||
Environment.ONE_WIRE_TEMP_3,
|
||||
Environment.ONE_WIRE_TEMP_4,
|
||||
Environment.ONE_WIRE_TEMP_5,
|
||||
Environment.ONE_WIRE_TEMP_6,
|
||||
Environment.ONE_WIRE_TEMP_7,
|
||||
Environment.ONE_WIRE_TEMP_8,
|
||||
)
|
||||
.mapIndexed { index, entry ->
|
||||
LegendData(
|
||||
nameRes = Res.string.one_wire_temperature,
|
||||
labelOverride = "1-Wire Temp ${index + 1}",
|
||||
color = entry.color,
|
||||
isLine = true,
|
||||
metricKey = entry,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
fun EnvironmentMetricsChart(
|
||||
|
|
@ -132,7 +154,7 @@ fun EnvironmentMetricsChart(
|
|||
val onSurfaceColor = MaterialTheme.colorScheme.onSurface
|
||||
|
||||
val allLegendData =
|
||||
(LEGEND_DATA_1 + LEGEND_DATA_2 + LEGEND_DATA_3).filter {
|
||||
(LEGEND_DATA_1 + LEGEND_DATA_2 + LEGEND_DATA_3 + LEGEND_DATA_4).filter {
|
||||
graphData.shouldPlot[(it.metricKey as? Environment)?.ordinal ?: 0]
|
||||
}
|
||||
|
||||
|
|
@ -143,7 +165,7 @@ fun EnvironmentMetricsChart(
|
|||
hiddenIndices.mapNotNull { allLegendData.getOrNull(it)?.metricKey as? Environment }.toSet()
|
||||
}
|
||||
|
||||
val colorToLabel = allLegendData.associate { it.color to stringResource(it.nameRes) }
|
||||
val colorToLabel = allLegendData.associate { it.color to (it.labelOverride ?: stringResource(it.nameRes)) }
|
||||
|
||||
val showPressure =
|
||||
shouldPlot[Environment.BAROMETRIC_PRESSURE.ordinal] && Environment.BAROMETRIC_PRESSURE !in hiddenMetrics
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ import org.meshtastic.core.resources.humidity
|
|||
import org.meshtastic.core.resources.iaq
|
||||
import org.meshtastic.core.resources.iaq_definition
|
||||
import org.meshtastic.core.resources.lux
|
||||
import org.meshtastic.core.resources.one_wire_temperature
|
||||
import org.meshtastic.core.resources.radiation
|
||||
import org.meshtastic.core.resources.rainfall_1h
|
||||
import org.meshtastic.core.resources.rainfall_24h
|
||||
|
|
@ -443,6 +444,39 @@ private fun RainfallDisplay(envMetrics: org.meshtastic.proto.EnvironmentMetrics)
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OneWireTemperatureDisplay(
|
||||
envMetrics: org.meshtastic.proto.EnvironmentMetrics,
|
||||
environmentDisplayFahrenheit: Boolean,
|
||||
) {
|
||||
val sensors = envMetrics.one_wire_temperature.filterNot { it.isNaN() }
|
||||
if (sensors.isEmpty()) return
|
||||
val oneWireEntries =
|
||||
listOf(
|
||||
Environment.ONE_WIRE_TEMP_1,
|
||||
Environment.ONE_WIRE_TEMP_2,
|
||||
Environment.ONE_WIRE_TEMP_3,
|
||||
Environment.ONE_WIRE_TEMP_4,
|
||||
Environment.ONE_WIRE_TEMP_5,
|
||||
Environment.ONE_WIRE_TEMP_6,
|
||||
Environment.ONE_WIRE_TEMP_7,
|
||||
Environment.ONE_WIRE_TEMP_8,
|
||||
)
|
||||
val textFormat = if (environmentDisplayFahrenheit) "%s %d: %.1f°F" else "%s %d: %.1f°C"
|
||||
sensors.forEachIndexed { idx, temp ->
|
||||
val color = oneWireEntries.getOrNull(idx)?.color ?: Environment.ONE_WIRE_TEMP_1.color
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
MetricIndicator(color)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(
|
||||
text = formatString(textFormat, stringResource(Res.string.one_wire_temperature), idx + 1, temp),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontSize = MaterialTheme.typography.labelLarge.fontSize,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EnvironmentMetricsCard(
|
||||
telemetry: Telemetry,
|
||||
|
|
@ -484,6 +518,7 @@ private fun EnvironmentMetricsContent(telemetry: Telemetry, environmentDisplayFa
|
|||
RadiationDisplay(envMetrics)
|
||||
WindDisplay(envMetrics)
|
||||
RainfallDisplay(envMetrics)
|
||||
OneWireTemperatureDisplay(envMetrics, environmentDisplayFahrenheit)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,16 +18,24 @@ package org.meshtastic.feature.node.metrics
|
|||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import org.meshtastic.core.model.util.UnitConversions
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Amber
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Blue
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Chartreuse
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Coral
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Cyan
|
||||
import org.meshtastic.core.ui.theme.GraphColors.DeepOrange
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Gold
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Green
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Indigo
|
||||
import org.meshtastic.core.ui.theme.GraphColors.InfantryBlue
|
||||
import org.meshtastic.core.ui.theme.GraphColors.LightGreen
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Lime
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Magenta
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Orange
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Pink
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Purple
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Red
|
||||
import org.meshtastic.core.ui.theme.GraphColors.SkyBlue
|
||||
import org.meshtastic.core.ui.theme.GraphColors.Teal
|
||||
import org.meshtastic.proto.Telemetry
|
||||
|
||||
|
|
@ -66,7 +74,39 @@ enum class Environment(val color: Color) {
|
|||
override fun getValue(telemetry: Telemetry) = telemetry.environment_metrics?.wind_speed
|
||||
},
|
||||
RADIATION(Lime) {
|
||||
override fun getValue(telemetry: Telemetry) = telemetry.environment_metrics?.radiation
|
||||
override fun getValue(telemetry: Telemetry): Float? = telemetry.environment_metrics?.radiation
|
||||
},
|
||||
ONE_WIRE_TEMP_1(Amber) {
|
||||
override fun getValue(telemetry: Telemetry): Float? =
|
||||
telemetry.environment_metrics?.one_wire_temperature?.getOrNull(0)
|
||||
},
|
||||
ONE_WIRE_TEMP_2(DeepOrange) {
|
||||
override fun getValue(telemetry: Telemetry): Float? =
|
||||
telemetry.environment_metrics?.one_wire_temperature?.getOrNull(1)
|
||||
},
|
||||
ONE_WIRE_TEMP_3(Indigo) {
|
||||
override fun getValue(telemetry: Telemetry): Float? =
|
||||
telemetry.environment_metrics?.one_wire_temperature?.getOrNull(2)
|
||||
},
|
||||
ONE_WIRE_TEMP_4(LightGreen) {
|
||||
override fun getValue(telemetry: Telemetry): Float? =
|
||||
telemetry.environment_metrics?.one_wire_temperature?.getOrNull(3)
|
||||
},
|
||||
ONE_WIRE_TEMP_5(Magenta) {
|
||||
override fun getValue(telemetry: Telemetry): Float? =
|
||||
telemetry.environment_metrics?.one_wire_temperature?.getOrNull(4)
|
||||
},
|
||||
ONE_WIRE_TEMP_6(SkyBlue) {
|
||||
override fun getValue(telemetry: Telemetry): Float? =
|
||||
telemetry.environment_metrics?.one_wire_temperature?.getOrNull(5)
|
||||
},
|
||||
ONE_WIRE_TEMP_7(Chartreuse) {
|
||||
override fun getValue(telemetry: Telemetry): Float? =
|
||||
telemetry.environment_metrics?.one_wire_temperature?.getOrNull(6)
|
||||
},
|
||||
ONE_WIRE_TEMP_8(Coral) {
|
||||
override fun getValue(telemetry: Telemetry): Float? =
|
||||
telemetry.environment_metrics?.one_wire_temperature?.getOrNull(7)
|
||||
}, ;
|
||||
|
||||
abstract fun getValue(telemetry: Telemetry): Float?
|
||||
|
|
@ -205,6 +245,33 @@ data class EnvironmentMetricsState(val environmentMetrics: List<Telemetry> = emp
|
|||
shouldPlot[Environment.RADIATION.ordinal] = true
|
||||
}
|
||||
|
||||
// 1-Wire temperature sensors (up to 8 channels, Fahrenheit-aware)
|
||||
val oneWireEntries =
|
||||
listOf(
|
||||
Environment.ONE_WIRE_TEMP_1,
|
||||
Environment.ONE_WIRE_TEMP_2,
|
||||
Environment.ONE_WIRE_TEMP_3,
|
||||
Environment.ONE_WIRE_TEMP_4,
|
||||
Environment.ONE_WIRE_TEMP_5,
|
||||
Environment.ONE_WIRE_TEMP_6,
|
||||
Environment.ONE_WIRE_TEMP_7,
|
||||
Environment.ONE_WIRE_TEMP_8,
|
||||
)
|
||||
oneWireEntries.forEach { entry ->
|
||||
val values = telemetries.mapNotNull { entry.getValue(it)?.takeIf { v -> !v.isNaN() } }
|
||||
if (values.isNotEmpty()) {
|
||||
var minVal = values.minOf { it }
|
||||
var maxVal = values.maxOf { it }
|
||||
if (useFahrenheit) {
|
||||
minVal = UnitConversions.celsiusToFahrenheit(minVal)
|
||||
maxVal = UnitConversions.celsiusToFahrenheit(maxVal)
|
||||
}
|
||||
minValues.add(minVal)
|
||||
maxValues.add(maxVal)
|
||||
shouldPlot[entry.ordinal] = true
|
||||
}
|
||||
}
|
||||
|
||||
val min = if (minValues.isEmpty()) 0f else minValues.minOf { it }
|
||||
val max = if (maxValues.isEmpty()) 1f else maxValues.maxOf { it }
|
||||
|
||||
|
|
|
|||
|
|
@ -148,6 +148,8 @@ open class MetricsViewModel(
|
|||
temperature = em.temperature?.let { UnitConversions.celsiusToFahrenheit(it) },
|
||||
soil_temperature =
|
||||
em.soil_temperature?.let { UnitConversions.celsiusToFahrenheit(it) },
|
||||
one_wire_temperature =
|
||||
em.one_wire_temperature.map { UnitConversions.celsiusToFahrenheit(it) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -381,21 +383,25 @@ open class MetricsViewModel(
|
|||
}
|
||||
|
||||
fun saveEnvironmentMetricsCSV(uri: MeshtasticUri, data: List<Telemetry>) {
|
||||
val oneWireHeaders = (1..ONE_WIRE_SENSOR_COUNT).joinToString(",") { "\"oneWireTemp$it\"" }
|
||||
exportCsv(
|
||||
uri = uri,
|
||||
header =
|
||||
"\"date\",\"time\",\"temperature\",\"relativeHumidity\",\"barometricPressure\"," +
|
||||
"\"gasResistance\",\"iaq\",\"windSpeed\",\"windDirection\",\"soilTemperature\"," +
|
||||
"\"soilMoisture\"\n",
|
||||
"\"soilMoisture\",$oneWireHeaders\n",
|
||||
rows = data,
|
||||
epochSeconds = { it.time.toLong() },
|
||||
) { t ->
|
||||
val em = t.environment_metrics
|
||||
val owt = em?.one_wire_temperature ?: emptyList()
|
||||
val oneWireValues =
|
||||
(0 until ONE_WIRE_SENSOR_COUNT).joinToString(",") { i -> "\"${owt.getOrNull(i) ?: ""}\"" }
|
||||
"\"${em?.temperature ?: ""}\",\"${em?.relative_humidity ?: ""}\"," +
|
||||
"\"${em?.barometric_pressure ?: ""}\",\"${em?.gas_resistance ?: ""}\"," +
|
||||
"\"${em?.iaq ?: ""}\",\"${em?.wind_speed ?: ""}\"," +
|
||||
"\"${em?.wind_direction ?: ""}\",\"${em?.soil_temperature ?: ""}\"," +
|
||||
"\"${em?.soil_moisture ?: ""}\""
|
||||
"\"${em?.soil_moisture ?: ""}\",$oneWireValues"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -457,4 +463,8 @@ open class MetricsViewModel(
|
|||
}
|
||||
|
||||
protected fun decodeBase64(base64: String): ByteArray = base64.decodeBase64()?.toByteArray() ?: ByteArray(0)
|
||||
|
||||
companion object {
|
||||
private const val ONE_WIRE_SENSOR_COUNT = 8
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue