From 2b83d99820630572f15751a4a703e81c7b5157c3 Mon Sep 17 00:00:00 2001 From: ju6ge Date: Sun, 10 May 2026 14:04:51 +0200 Subject: [PATCH 1/5] fix: serialize firmware/state as JSON instead of Debug format --- rust/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/src/main.rs b/rust/src/main.rs index f8266d2..9d19afb 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -739,7 +739,7 @@ async fn publish_firmware_info( timezone_time: &str, ) { mqtt::publish("/firmware/address", ip_address).await; - mqtt::publish("/firmware/state", format!("{:?}", &version).as_str()) + mqtt::publish("/firmware/state", &serde_json::to_string(&version).unwrap()) .await; mqtt::publish("/firmware/last_online", timezone_time) .await; From 43a0c3c274eee17761d3b490e3facead398730b2 Mon Sep 17 00:00:00 2001 From: ju6ge Date: Sun, 10 May 2026 14:04:54 +0200 Subject: [PATCH 2/5] refactor: BatteryInfo structure (consistent layout) - use tagged enum serialization for BatteryError - flatten BatteryInfo telemetry with consistent field names and typed error --- rust/src/hal/battery.rs | 37 +++++++++++++++++++----------------- rust/src/main.rs | 42 ++++++++++++++++++++++++++++++----------- 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/rust/src/hal/battery.rs b/rust/src/hal/battery.rs index 2165970..0ddb272 100644 --- a/rust/src/hal/battery.rs +++ b/rust/src/hal/battery.rs @@ -27,20 +27,22 @@ pub trait BatteryInteraction { #[derive(Debug, Serialize)] pub struct BatteryInfo { - pub voltage_milli_volt: u16, - pub average_current_milli_ampere: i16, - pub cycle_count: u16, - pub design_milli_ampere_hour: u16, - pub remaining_milli_ampere_hour: u16, - pub state_of_charge: f32, - pub state_of_health: u16, - pub temperature: u16, + pub voltage_mv: Option, + pub avg_current_ma: Option, + pub soc_pct: Option, + pub soh_pct: Option, + pub temperature_c: Option, + pub cycle_count: Option, + pub remaining_mah: Option, + pub design_mah: Option, + pub error: Option, } #[derive(Debug, Serialize)] +#[serde(tag = "kind")] pub enum BatteryError { NoBatteryMonitor, - CommunicationError(String), + CommunicationError { message: String }, } #[derive(Debug, Serialize)] @@ -180,14 +182,15 @@ impl BatteryInteraction for BQ34Z100G1 { async fn get_battery_state(&mut self) -> FatResult { Ok(BatteryState::Info(BatteryInfo { - voltage_milli_volt: self.voltage_milli_volt().await?, - average_current_milli_ampere: self.average_current_milli_ampere().await?, - cycle_count: self.cycle_count().await?, - design_milli_ampere_hour: self.design_milli_ampere_hour().await?, - remaining_milli_ampere_hour: self.remaining_milli_ampere_hour().await?, - state_of_charge: self.state_charge_percent().await?, - state_of_health: self.state_health_percent().await?, - temperature: self.bat_temperature().await?, + voltage_mv: Some(self.voltage_milli_volt().await?), + avg_current_ma: Some(self.average_current_milli_ampere().await?), + soc_pct: Some(self.state_charge_percent().await?), + soh_pct: Some(self.state_health_percent().await?), + temperature_c: Some(self.bat_temperature().await?), + cycle_count: Some(self.cycle_count().await?), + remaining_mah: Some(self.remaining_milli_ampere_hour().await?), + design_mah: Some(self.design_milli_ampere_hour().await?), + error: None, })) } } diff --git a/rust/src/main.rs b/rust/src/main.rs index 9d19afb..135b5d8 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -43,7 +43,7 @@ use embassy_time::{Duration, Instant, Timer}; use esp_hal::rom::ets_delay_us; use esp_hal::system::software_reset; use esp_println::{logger, println}; -use hal::battery::BatteryState; +use hal::battery::{BatteryError, BatteryInfo, BatteryState}; use log::LogMessage; use option_lock::OptionLock; use plant_state::PlantState; @@ -791,20 +791,40 @@ async fn publish_mppt_state( async fn publish_battery_state( board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, ) -> () { - let state = board + let telemetry = match board .board_hal .get_battery_monitor() .get_battery_state() - .await; - let value = match state { - Ok(state) => { - let json = serde_json::to_string(&state).unwrap().to_owned(); - json.to_owned() - } - Err(_) => "error".to_owned(), - }; + .await { - let _ = mqtt::publish("/battery", &*value).await; + Ok(BatteryState::Info(info)) => info, + Ok(BatteryState::Unknown) => BatteryInfo { + voltage_mv: None, + avg_current_ma: None, + soc_pct: None, + soh_pct: None, + temperature_c: None, + cycle_count: None, + remaining_mah: None, + design_mah: None, + error: Some(BatteryError::NoBatteryMonitor), + }, + Err(e) => BatteryInfo { + voltage_mv: None, + avg_current_ma: None, + soc_pct: None, + soh_pct: None, + temperature_c: None, + cycle_count: None, + remaining_mah: None, + design_mah: None, + error: Some(BatteryError::CommunicationError { + message: alloc::format!("{:?}", e), + }), + }, + }; + if let Ok(json) = serde_json::to_string(&telemetry) { + let _ = mqtt::publish("/battery", &json).await; } } From e2b2734301acdfd2d4181b218515062f89ce66ea Mon Sep 17 00:00:00 2001 From: ju6ge Date: Sun, 10 May 2026 14:04:55 +0200 Subject: [PATCH 3/5] 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 --- rust/src/plant_state.rs | 68 +++++++++++++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 16 deletions(-) diff --git a/rust/src/plant_state.rs b/rust/src/plant_state.rs index 26c7288..1560719 100644 --- a/rust/src/plant_state.rs +++ b/rust/src/plant_state.rs @@ -11,11 +11,12 @@ use serde::{Deserialize, Serialize}; const MOIST_SENSOR_MAX_FREQUENCY: f32 = 7500.; // 60kHz (500Hz margin) const MOIST_SENSOR_MIN_FREQUENCY: f32 = 150.; // this is really, really dry, think like cactus levels -#[derive(Debug, PartialEq, Serialize)] +#[derive(Debug, PartialEq, Clone, Serialize)] +#[serde(tag = "kind")] pub enum MoistureSensorError { ShortCircuit { hz: f32, max: f32 }, OpenLoop { hz: f32, min: f32 }, - BoardError(String), + BoardError { message: String }, } #[derive(Debug, PartialEq, Serialize)] @@ -49,6 +50,14 @@ impl MoistureSensorState { impl MoistureSensorState {} #[derive(Debug, PartialEq, Serialize)] +pub struct SensorTelemetry { + pub moisture_pct: Option, + pub raw_hz: Option, + pub error: Option, +} + +#[derive(Debug, PartialEq, Serialize)] +#[serde(tag = "kind")] pub enum PumpError { PumpNotWorking { failed_attempts: usize, @@ -134,9 +143,9 @@ impl PlantState { }, Err(err) => MoistureSensorState::SensorError(err), }, - Err(err) => MoistureSensorState::SensorError(MoistureSensorError::BoardError( - err.to_string(), - )), + Err(err) => MoistureSensorState::SensorError(MoistureSensorError::BoardError { + message: err.to_string(), + }) } } else { MoistureSensorState::Disabled @@ -159,9 +168,9 @@ impl PlantState { }, Err(err) => MoistureSensorState::SensorError(err), }, - Err(err) => MoistureSensorState::SensorError(MoistureSensorError::BoardError( - err.to_string(), - )), + Err(err) => MoistureSensorState::SensorError(MoistureSensorError::BoardError { + message: err.to_string(), + }) } } else { MoistureSensorState::Disabled @@ -277,19 +286,21 @@ impl PlantState { &self, plant_conf: &PlantConfig, current_time: &DateTime, - ) -> PlantInfo<'_> { + ) -> PlantInfo { + let (moisture_pct, _) = self.plant_moisture(); PlantInfo { - sensor_a: &self.sensor_a, - sensor_b: &self.sensor_b, + moisture_pct, + sensor_a: Self::sensor_to_telemetry(&self.sensor_a), + sensor_b: Self::sensor_to_telemetry(&self.sensor_b), mode: plant_conf.mode, 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 } else { false }, 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, plant_conf.pump_hour_start, plant_conf.pump_hour_end, @@ -316,15 +327,40 @@ impl PlantState { }, } } + + fn sensor_to_telemetry(sensor: &MoistureSensorState) -> SensorTelemetry { + match sensor { + MoistureSensorState::Disabled => SensorTelemetry { + moisture_pct: None, + raw_hz: None, + error: None, + }, + MoistureSensorState::MoistureValue { + raw_hz, + moisture_percent, + } => SensorTelemetry { + moisture_pct: Some(*moisture_percent), + raw_hz: Some(*raw_hz), + error: None, + }, + MoistureSensorState::SensorError(err) => SensorTelemetry { + moisture_pct: None, + raw_hz: None, + error: Some(err.clone()), + }, + } + } } #[derive(Debug, PartialEq, Serialize)] /// State of a single plant to be tracked -pub struct PlantInfo<'a> { +pub struct PlantInfo { + /// combined plant moisture from available sensors + moisture_pct: Option, /// state of humidity sensor on bank a - sensor_a: &'a MoistureSensorState, + sensor_a: SensorTelemetry, /// state of humidity sensor on bank b - sensor_b: &'a MoistureSensorState, + sensor_b: SensorTelemetry, /// configured plant watering mode mode: PlantWateringMode, /// the plant needs to be watered From 379808e659a06d18ebf180d48e9ac43700878be3 Mon Sep 17 00:00:00 2001 From: ju6ge Date: Sun, 10 May 2026 14:04:57 +0200 Subject: [PATCH 4/5] refctor: TankInfo structure (consistent layout) - fix: use tagged enum serialization for TankError - fix: rename TankInfo fields for consistent naming (volume_ml, pct, water_temp_c) - renamed some fields for better clarity on contained value --- rust/src/main.rs | 6 +++--- rust/src/tank.rs | 25 +++++++++++++------------ 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/rust/src/main.rs b/rust/src/main.rs index 135b5d8..4e64e73 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -317,7 +317,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { if let Some(err) = tank_state.got_error(&board.board_hal.get_config().tank) { match err { TankError::SensorDisabled => { /* unreachable */ } - TankError::SensorMissing(raw_value_mv) => log( + TankError::SensorMissing { raw_mv: raw_value_mv } => log( LogMessage::TankSensorMissing, raw_value_mv as u32, 0, @@ -331,8 +331,8 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { &format!("{value}"), "", ), - TankError::BoardError(err) => { - log(LogMessage::TankSensorBoardError, 0, 0, "", &err.to_string()) + TankError::BoardError { message: err } => { + log(LogMessage::TankSensorBoardError, 0, 0, "", &err) } } // disabled cannot trigger this because of wrapping if is_enabled diff --git a/rust/src/tank.rs b/rust/src/tank.rs index 38dd4ca..6d6b345 100644 --- a/rust/src/tank.rs +++ b/rust/src/tank.rs @@ -10,11 +10,12 @@ const OPEN_TANK_VOLTAGE: f32 = 3.0; pub const WATER_FROZEN_THRESH: f32 = 4.0; #[derive(Debug, Clone, Serialize)] +#[serde(tag = "kind")] pub enum TankError { SensorDisabled, - SensorMissing(f32), + SensorMissing { raw_mv: f32 }, SensorValueError { value: f32, min: f32, max: f32 }, - BoardError(String), + BoardError { message: String }, } pub enum TankState { @@ -25,7 +26,7 @@ pub enum TankState { fn raw_voltage_to_divider_percent(raw_value_mv: f32) -> Result { if raw_value_mv > OPEN_TANK_VOLTAGE { - return Err(TankError::SensorMissing(raw_value_mv)); + return Err(TankError::SensorMissing { raw_mv: raw_value_mv }); } let r2 = raw_value_mv * 50.0 / (3.3 - raw_value_mv); @@ -141,15 +142,15 @@ impl TankState { TankInfo { enough_water, warn_level, - left_ml, + volume_ml: left_ml, sensor_error: tank_err, - raw, + fill_raw_v: raw, water_frozen: water_temp .as_ref() .is_ok_and(|temp| *temp < WATER_FROZEN_THRESH), - water_temp: water_temp.as_ref().copied().ok(), + water_temp_c: water_temp.as_ref().copied().ok(), temp_sensor_error: water_temp.as_ref().err().map(|err| err.to_string()), - percent, + fill_pct: percent, } } } @@ -164,7 +165,7 @@ pub async fn determine_tank_state( .and_then(|f| core::prelude::v1::Ok(f.tank_sensor_voltage())) { Ok(raw_sensor_value_mv) => TankState::Present(raw_sensor_value_mv.await.unwrap()), - Err(err) => TankState::Error(TankError::BoardError(err.to_string())), + Err(err) => TankState::Error(TankError::BoardError { message: err.to_string() }), } } else { TankState::Disabled @@ -179,16 +180,16 @@ pub struct TankInfo { /// warning that water needs to be refilled soon pub(crate) warn_level: bool, /// estimation how many ml are still in the tank - pub(crate) left_ml: Option, + pub(crate) volume_ml: Option, /// if there is an issue with the water level sensor pub(crate) sensor_error: Option, /// raw water sensor value - pub(crate) raw: Option, + pub(crate) fill_raw_v: Option, /// percent value - pub(crate) percent: Option, + pub(crate) fill_pct: Option, /// water in the tank might be frozen pub(crate) water_frozen: bool, /// water temperature - pub(crate) water_temp: Option, + pub(crate) water_temp_c: Option, pub(crate) temp_sensor_error: Option, } From a66843a455f1039b9d2ad476bad8c462d9830324 Mon Sep 17 00:00:00 2001 From: ju6ge Date: Sun, 10 May 2026 14:32:00 +0200 Subject: [PATCH 5/5] move all mqtt publishing functions to mqtt module --- rust/src/main.rs | 139 ++------------------------------------- rust/src/mqtt.rs | 167 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 156 insertions(+), 150 deletions(-) diff --git a/rust/src/main.rs b/rust/src/main.rs index 4e64e73..c6d3b57 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -272,9 +272,9 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { ); if let network::NetworkMode::WIFI { ref ip_address, .. } = network_mode { - publish_firmware_info(&mut board, version, ip_address, &timezone_time.to_rfc3339()).await; - publish_battery_state(&mut board).await; - let _ = publish_mppt_state(&mut board).await; + mqtt::publish_firmware_info(&mut board, version, ip_address, &timezone_time.to_rfc3339()).await; + mqtt::publish_battery_state(&mut board).await; + let _ = mqtt::publish_mppt_state(&mut board).await; } log( @@ -359,7 +359,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { } info!("Water temp is {}", water_temp.as_ref().unwrap_or(&0.)); - publish_tank_state(&mut board, &tank_state, water_temp).await; + mqtt::publish_tank_state(&mut board, &tank_state, water_temp).await; let plantstate: [PlantState; PLANT_COUNT] = [ PlantState::read_hardware_state(0, &mut board).await, @@ -372,7 +372,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { PlantState::read_hardware_state(7, &mut board).await, ]; - publish_plant_states(&mut board, &timezone_time.clone(), &plantstate).await; + mqtt::publish_plant_states(&mut board, &timezone_time.clone(), &plantstate).await; let pump_required = plantstate .iter() @@ -415,10 +415,10 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { .get_esp() .store_last_pump_time(plant_id, cur); board.board_hal.get_esp().last_pump_time(plant_id); - pump_info(plant_id, true, pump_ineffective, 0, 0, 0, false).await; + mqtt::pump_info(plant_id, true, pump_ineffective, 0, 0, 0, false).await; let result = do_secure_pump(&mut board, plant_id, plant_config, dry_run).await?; board.board_hal.pump(plant_id, false).await?; - pump_info( + mqtt::pump_info( plant_id, false, pump_ineffective, @@ -703,131 +703,6 @@ async fn update_charge_indicator( Ok(()) } -async fn publish_tank_state( - board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, - tank_state: &TankState, - water_temp: FatResult, -) { - let state = serde_json::to_string( - &tank_state.as_mqtt_info(&board.board_hal.get_config().tank, &water_temp), - ) - .unwrap(); - let _ = mqtt::publish("/water", &*state).await; -} - -async fn publish_plant_states( - board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, - timezone_time: &DateTime, - plantstate: &[PlantState; 8], -) { - for (plant_id, (plant_state, plant_conf)) in plantstate - .iter() - .zip(&board.board_hal.get_config().plants.clone()) - .enumerate() - { - let state = - serde_json::to_string(&plant_state.to_mqtt_info(plant_conf, timezone_time)).unwrap(); - let plant_topic = format!("/plant{}", plant_id + 1); - let _ = mqtt::publish(&plant_topic, &state).await; - } -} - -async fn publish_firmware_info( - board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, - version: VersionInfo, - ip_address: &str, - timezone_time: &str, -) { - mqtt::publish("/firmware/address", ip_address).await; - mqtt::publish("/firmware/state", &serde_json::to_string(&version).unwrap()) - .await; - mqtt::publish("/firmware/last_online", timezone_time) - .await; - mqtt::publish("/state", "online").await; -} -async fn pump_info( - plant_id: usize, - pump_active: bool, - pump_ineffective: bool, - median_current_ma: u16, - max_current_ma: u16, - min_current_ma: u16, - _error: bool, -) { - let pump_info = mqtt::PumpInfo { - enabled: pump_active, - pump_ineffective, - median_current_ma, - max_current_ma, - min_current_ma, - }; - let pump_topic = format!("/pump{}", plant_id + 1); - - match serde_json::to_string(&pump_info) { - Ok(state) => { - let _ = mqtt::publish(&pump_topic, &state).await; - } - Err(err) => { - warn!("Error publishing pump state {}", err); - } - }; -} - -async fn publish_mppt_state( - board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, -) -> FatResult<()> { - let current = board.board_hal.get_mptt_current().await?; - let voltage = board.board_hal.get_mptt_voltage().await?; - let solar_state = mqtt::Solar { - current_ma: current.as_milliamperes() as u32, - voltage_ma: voltage.as_millivolts() as u32, - }; - if let Ok(serialized_solar_state_bytes) = serde_json::to_string(&solar_state) { - let _ = mqtt::publish("/mppt", &serialized_solar_state_bytes).await; - } - Ok(()) -} - -async fn publish_battery_state( - board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, -) -> () { - let telemetry = match board - .board_hal - .get_battery_monitor() - .get_battery_state() - .await - { - Ok(BatteryState::Info(info)) => info, - Ok(BatteryState::Unknown) => BatteryInfo { - voltage_mv: None, - avg_current_ma: None, - soc_pct: None, - soh_pct: None, - temperature_c: None, - cycle_count: None, - remaining_mah: None, - design_mah: None, - error: Some(BatteryError::NoBatteryMonitor), - }, - Err(e) => BatteryInfo { - voltage_mv: None, - avg_current_ma: None, - soc_pct: None, - soh_pct: None, - temperature_c: None, - cycle_count: None, - remaining_mah: None, - design_mah: None, - error: Some(BatteryError::CommunicationError { - message: alloc::format!("{:?}", e), - }), - }, - }; - if let Ok(json) = serde_json::to_string(&telemetry) { - let _ = mqtt::publish("/battery", &json).await; - } -} - async fn wait_infinity( board: MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, wait_type: WaitType, diff --git a/rust/src/mqtt.rs b/rust/src/mqtt.rs index 2d5718f..052d12f 100644 --- a/rust/src/mqtt.rs +++ b/rust/src/mqtt.rs @@ -1,16 +1,23 @@ -use crate::bail; use crate::config::NetworkConfig; use crate::fat_error::{ContextExt, FatError, FatResult}; -use crate::hal::PlantHal; +use crate::hal::battery::{BatteryError, BatteryInfo, BatteryState}; +use crate::hal::{PlantHal, HAL}; use crate::log::{log, LogMessage}; +use crate::plant_state::PlantState; +use crate::tank::TankState; +use crate::{bail, VersionInfo}; use alloc::string::String; use alloc::{format, string::ToString}; +use chrono::DateTime; +use chrono_tz::Tz; use core::sync::atomic::Ordering; use embassy_executor::Spawner; use embassy_net::Stack; +use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; +use embassy_sync::mutex::MutexGuard; use embassy_sync::once_lock::OnceLock; use embassy_time::{Duration, Timer, WithTimeout}; -use log::info; +use log::{info, warn}; use mcutie::{ Error, McutieBuilder, McutieReceiver, McutieTask, MqttMessage, PublishDisplay, Publishable, QoS, Topic, @@ -18,21 +25,6 @@ use mcutie::{ use portable_atomic::AtomicBool; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] -pub struct PumpInfo { - pub enabled: bool, - pub pump_ineffective: bool, - pub median_current_ma: u16, - pub max_current_ma: u16, - pub min_current_ma: u16, -} - -#[derive(Serialize, Debug, PartialEq)] -pub struct Solar { - pub current_ma: u32, - pub voltage_ma: u32, -} - static MQTT_CONNECTED_EVENT_RECEIVED: AtomicBool = AtomicBool::new(false); static MQTT_ROUND_TRIP_RECEIVED: AtomicBool = AtomicBool::new(false); pub static MQTT_STAY_ALIVE: AtomicBool = AtomicBool::new(false); @@ -265,3 +257,142 @@ async fn mqtt_incoming_task( } } } + +pub async fn publish_tank_state( + board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, + tank_state: &TankState, + water_temp: FatResult, +) { + let state = serde_json::to_string( + &tank_state.as_mqtt_info(&board.board_hal.get_config().tank, &water_temp), + ) + .unwrap(); + let _ = publish("/water", &*state).await; +} + +pub async fn publish_plant_states( + board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, + timezone_time: &DateTime, + plantstate: &[PlantState; 8], +) { + for (plant_id, (plant_state, plant_conf)) in plantstate + .iter() + .zip(&board.board_hal.get_config().plants.clone()) + .enumerate() + { + let state = + serde_json::to_string(&plant_state.to_mqtt_info(plant_conf, timezone_time)).unwrap(); + let plant_topic = format!("/plant{}", plant_id + 1); + let _ = publish(&plant_topic, &state).await; + } +} + +pub async fn publish_firmware_info( + board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, + version: VersionInfo, + ip_address: &str, + timezone_time: &str, +) { + publish("/firmware/address", ip_address).await; + publish("/firmware/state", &serde_json::to_string(&version).unwrap()).await; + publish("/firmware/last_online", timezone_time).await; + publish("/state", "online").await; +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] +struct PumpInfo { + pub enabled: bool, + pub pump_ineffective: bool, + pub median_current_ma: u16, + pub max_current_ma: u16, + pub min_current_ma: u16, +} + +pub async fn pump_info( + plant_id: usize, + pump_active: bool, + pump_ineffective: bool, + median_current_ma: u16, + max_current_ma: u16, + min_current_ma: u16, + _error: bool, +) { + let pump_info = PumpInfo { + enabled: pump_active, + pump_ineffective, + median_current_ma, + max_current_ma, + min_current_ma, + }; + let pump_topic = format!("/pump{}", plant_id + 1); + + match serde_json::to_string(&pump_info) { + Ok(state) => { + let _ = publish(&pump_topic, &state).await; + } + Err(err) => { + warn!("Error publishing pump state {}", err); + } + }; +} + +#[derive(Serialize, Debug, PartialEq)] +pub struct Solar { + pub current_ma: u32, + pub voltage_ma: u32, +} + +pub async fn publish_mppt_state( + board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, +) -> FatResult<()> { + let current = board.board_hal.get_mptt_current().await?; + let voltage = board.board_hal.get_mptt_voltage().await?; + let solar_state = Solar { + current_ma: current.as_milliamperes() as u32, + voltage_ma: voltage.as_millivolts() as u32, + }; + if let Ok(serialized_solar_state_bytes) = serde_json::to_string(&solar_state) { + let _ = publish("/mppt", &serialized_solar_state_bytes).await; + } + Ok(()) +} + +pub async fn publish_battery_state( + board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, +) -> () { + let telemetry = match board + .board_hal + .get_battery_monitor() + .get_battery_state() + .await + { + Ok(BatteryState::Info(info)) => info, + Ok(BatteryState::Unknown) => BatteryInfo { + voltage_mv: None, + avg_current_ma: None, + soc_pct: None, + soh_pct: None, + temperature_c: None, + cycle_count: None, + remaining_mah: None, + design_mah: None, + error: Some(BatteryError::NoBatteryMonitor), + }, + Err(e) => BatteryInfo { + voltage_mv: None, + avg_current_ma: None, + soc_pct: None, + soh_pct: None, + temperature_c: None, + cycle_count: None, + remaining_mah: None, + design_mah: None, + error: Some(BatteryError::CommunicationError { + message: alloc::format!("{:?}", e), + }), + }, + }; + if let Ok(json) = serde_json::to_string(&telemetry) { + let _ = publish("/battery", &json).await; + } +}