refactor: PlantInfo structure (consistent layout)

- fix: use tagged enum serialization for MoistureSensorError and PumpError
- fix: flatten PlantInfo sensors to SensorTelemetry with top-level moisture_pct
This commit is contained in:
2026-05-10 14:04:55 +02:00
parent df3159aa16
commit 6bf7a04024

View File

@@ -1,5 +1,6 @@
use crate::hal::Moistures; use crate::hal::Moistures;
use crate::{config::PlantConfig, hal::HAL, in_time_range}; use crate::{config::PlantConfig, hal::HAL, in_time_range};
use alloc::string::String;
use chrono::{DateTime, TimeDelta, Utc}; use chrono::{DateTime, TimeDelta, Utc};
use chrono_tz::Tz; use chrono_tz::Tz;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -7,12 +8,14 @@ use serde::{Deserialize, Serialize};
const MOIST_SENSOR_MAX_FREQUENCY: f32 = 70000.; // 70kHz const MOIST_SENSOR_MAX_FREQUENCY: f32 = 70000.; // 70kHz
const MOIST_SENSOR_MIN_FREQUENCY: f32 = 400.; // this is really, really dry, think like cactus levels const MOIST_SENSOR_MIN_FREQUENCY: f32 = 400.; // this is really, really dry, think like cactus levels
#[derive(Debug, PartialEq, Serialize)] #[derive(Debug, PartialEq, Clone, Serialize)]
#[serde(tag = "kind")]
pub enum MoistureSensorError { pub enum MoistureSensorError {
MissingMessage, MissingMessage,
NotExpectedMessage { hz: f32 }, NotExpectedMessage { hz: f32 },
ShortCircuit { hz: f32, max: f32 }, ShortCircuit { hz: f32, max: f32 },
OpenLoop { hz: f32, min: f32 }, OpenLoop { hz: f32, min: f32 },
BoardError { message: String },
} }
#[derive(Debug, PartialEq, Serialize)] #[derive(Debug, PartialEq, Serialize)]
@@ -46,6 +49,14 @@ impl MoistureSensorState {
impl MoistureSensorState {} impl MoistureSensorState {}
#[derive(Debug, PartialEq, Serialize)] #[derive(Debug, PartialEq, Serialize)]
pub struct SensorTelemetry {
pub moisture_pct: Option<f32>,
pub raw_hz: Option<f32>,
pub error: Option<MoistureSensorError>,
}
#[derive(Debug, PartialEq, Serialize)]
#[serde(tag = "kind")]
pub enum PumpError { pub enum PumpError {
PumpNotWorking { PumpNotWorking {
failed_attempts: usize, failed_attempts: usize,
@@ -215,7 +226,7 @@ impl PlantState {
pub fn plant_moisture( pub fn plant_moisture(
&self, &self,
) -> ( ) -> (
Option<u8>, Option<f32>,
(Option<&MoistureSensorError>, Option<&MoistureSensorError>), (Option<&MoistureSensorError>, Option<&MoistureSensorError>),
) { ) {
match ( match (
@@ -223,13 +234,13 @@ impl PlantState {
self.sensor_b.moisture_percent(), self.sensor_b.moisture_percent(),
) { ) {
(Some(moisture_a), Some(moisture_b)) => { (Some(moisture_a), Some(moisture_b)) => {
(Some(((moisture_a + moisture_b) / 2.) as u8), (None, None)) (Some((moisture_a + moisture_b) / 2.), (None, None))
} }
(Some(moisture_percent), _) => { (Some(moisture_percent), _) => {
(Some(moisture_percent as u8), (None, self.sensor_b.is_err())) (Some(moisture_percent), (None, self.sensor_b.is_err()))
} }
(_, Some(moisture_percent)) => { (_, Some(moisture_percent)) => {
(Some(moisture_percent as u8), (self.sensor_a.is_err(), None)) (Some(moisture_percent), (self.sensor_a.is_err(), None))
} }
_ => (None, (self.sensor_a.is_err(), self.sensor_b.is_err())), _ => (None, (self.sensor_a.is_err(), self.sensor_b.is_err())),
} }
@@ -247,7 +258,7 @@ impl PlantState {
if let Some(moisture_percent) = moisture_percent { if let Some(moisture_percent) = moisture_percent {
if self.pump_in_timeout(plant_conf, current_time) { if self.pump_in_timeout(plant_conf, current_time) {
false false
} else if moisture_percent < plant_conf.target_moisture { } else if moisture_percent < plant_conf.target_moisture.into() {
in_time_range( in_time_range(
current_time, current_time,
plant_conf.pump_hour_start, plant_conf.pump_hour_start,
@@ -273,19 +284,21 @@ impl PlantState {
&self, &self,
plant_conf: &PlantConfig, plant_conf: &PlantConfig,
current_time: &DateTime<Tz>, current_time: &DateTime<Tz>,
) -> PlantInfo<'_> { ) -> PlantInfo {
let (moisture_pct, _) = self.plant_moisture();
PlantInfo { PlantInfo {
sensor_a: &self.sensor_a, moisture_pct,
sensor_b: &self.sensor_b, sensor_a: Self::sensor_to_telemetry(&self.sensor_a),
sensor_b: Self::sensor_to_telemetry(&self.sensor_b),
mode: plant_conf.mode, mode: plant_conf.mode,
do_water: self.needs_to_be_watered(plant_conf, current_time), do_water: self.needs_to_be_watered(plant_conf, current_time),
dry: if let Some(moisture_percent) = self.plant_moisture().0 { dry: if let Some(moisture_percent) = moisture_pct {
moisture_percent < plant_conf.target_moisture moisture_percent < plant_conf.target_moisture.into()
} else { } else {
false false
}, },
cooldown: self.pump_in_timeout(plant_conf, current_time), cooldown: self.pump_in_timeout(plant_conf, current_time),
out_of_work_hour: in_time_range( out_of_work_hour: !in_time_range(
current_time, current_time,
plant_conf.pump_hour_start, plant_conf.pump_hour_start,
plant_conf.pump_hour_end, plant_conf.pump_hour_end,
@@ -315,15 +328,42 @@ impl PlantState {
last_fertilizer_time: self.last_fertilizer_time, last_fertilizer_time: self.last_fertilizer_time,
} }
} }
fn sensor_to_telemetry(sensor: &MoistureSensorState) -> SensorTelemetry {
match sensor {
MoistureSensorState::NoMessage => {
SensorTelemetry {
moisture_pct: None,
raw_hz: None,
error: None
}
}
MoistureSensorState::MoistureValue {
hz,
moisture_percent,
} => SensorTelemetry {
moisture_pct: Some(*moisture_percent),
raw_hz: Some(*hz),
error: None,
},
MoistureSensorState::SensorError(err) => SensorTelemetry {
moisture_pct: None,
raw_hz: None,
error: Some(err.clone()),
},
}
}
} }
#[derive(Debug, PartialEq, Serialize)] #[derive(Debug, PartialEq, Serialize)]
/// State of a single plant to be tracked /// State of a single plant to be tracked
pub struct PlantInfo<'a> { pub struct PlantInfo {
/// combined plant moisture from available sensors
moisture_pct: Option<f32>,
/// state of humidity sensor on bank a /// state of humidity sensor on bank a
sensor_a: &'a MoistureSensorState, sensor_a: SensorTelemetry,
/// state of humidity sensor on bank b /// state of humidity sensor on bank b
sensor_b: &'a MoistureSensorState, sensor_b: SensorTelemetry,
/// configured plant watering mode /// configured plant watering mode
mode: PlantWateringMode, mode: PlantWateringMode,
/// the plant needs to be watered /// the plant needs to be watered