mqtt via mcutie

This commit is contained in:
2025-09-29 01:00:11 +02:00
parent 3d18b0dbf6
commit cfe23c8a09
15 changed files with 482 additions and 482 deletions

View File

@@ -13,17 +13,18 @@
esp_bootloader_esp_idf::esp_app_desc!();
use esp_backtrace as _;
use crate::config::PlantConfig;
use crate::config::{NetworkConfig, PlantConfig};
use crate::fat_error::FatResult;
use crate::hal::esp::MQTT_STAY_ALIVE;
use crate::hal::{esp_time, TIME_ACCESS};
use crate::log::LOG_ACCESS;
use crate::tank::{determine_tank_state, TankError, WATER_FROZEN_THRESH};
use crate::webserver::httpd;
use crate::tank::{determine_tank_state, TankError, TankState, WATER_FROZEN_THRESH};
use crate::webserver::http_server;
use crate::{
config::BoardVersion::INITIAL,
hal::{PlantHal, HAL, PLANT_COUNT},
};
use ::log::{error, info, warn};
use ::log::{info, warn};
use alloc::borrow::ToOwned;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
@@ -65,7 +66,6 @@ mod fat_error;
mod hal;
mod log;
mod plant_state;
mod sipo;
mod tank;
mod webserver;
@@ -74,8 +74,6 @@ extern crate alloc;
pub static BOARD_ACCESS: OnceLock<Mutex<CriticalSectionRawMutex, HAL<'static>>> = OnceLock::new();
pub static STAY_ALIVE: AtomicBool = AtomicBool::new(false);
#[derive(Serialize, Deserialize, Debug, PartialEq)]
enum WaitType {
MissingConfig,
@@ -167,7 +165,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
let cur = match board.board_hal.get_rtc_module().get_rtc_time().await {
Ok(value) => {
{
let mut guard = TIME_ACCESS.get().await.lock().await;
let guard = TIME_ACCESS.get().await.lock().await;
guard.set_current_time_us(value.timestamp_micros() as u64);
}
value
@@ -229,17 +227,17 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
info!("no mode override");
}
if (board.board_hal.get_config().hardware.board == INITIAL
&& board.board_hal.get_config().network.ssid.is_none())
if board.board_hal.get_config().hardware.board == INITIAL
&& board.board_hal.get_config().network.ssid.is_none()
{
info!("No wifi configured, starting initial config mode");
let stack = board.board_hal.get_esp().wifi_ap(false).await?;
let stack = board.board_hal.get_esp().wifi_ap().await?;
let reboot_now = Arc::new(AtomicBool::new(false));
println!("starting webserver");
spawner.spawn(httpd(reboot_now.clone(), stack))?;
spawner.spawn(http_server(reboot_now.clone(), stack))?;
wait_infinity(board, WaitType::MissingConfig, reboot_now.clone()).await;
}
@@ -248,7 +246,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
try_connect_wifi_sntp_mqtt(&mut board, &mut stack).await
} else {
info!("No wifi configured");
//the current sensors require this amount to stabilize, in case of wifi this is already handled due to connect timings;
//the current sensors require this amount to stabilize, in the case of Wi-Fi this is already handled due to connect timings;
Timer::after_millis(100).await;
NetworkMode::OFFLINE
};
@@ -258,7 +256,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
let res = {
let esp = board.board_hal.get_esp();
esp.wifi_ap(true).await
esp.wifi_ap().await
};
match res {
Ok(ap_stack) => {
@@ -277,7 +275,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
}),
None => UTC, // Fallback to UTC if no timezone is set
};
let _timezone = Tz::UTC;
let _timezone = UTC;
let timezone_time = cur.with_timezone(&timezone);
info!(
@@ -318,7 +316,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
info!("executing config mode override");
//config upload will trigger reboot!
let reboot_now = Arc::new(AtomicBool::new(false));
spawner.spawn(httpd(reboot_now.clone(), stack.take().unwrap()))?;
spawner.spawn(http_server(reboot_now.clone(), stack.take().unwrap()))?;
wait_infinity(board, WaitType::ConfigButton, reboot_now.clone()).await;
} else {
LOG_ACCESS
@@ -396,11 +394,11 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
_water_frozen = true;
}
}
info!("Water temp is {}", water_temp.unwrap_or(0.));
info!("Water temp is {}", water_temp.as_ref().unwrap_or(&0.));
//publish_tank_state(&tank_state, &water_temp).await;
publish_tank_state(&mut board, &tank_state, water_temp).await;
let _plantstate: [PlantState; PLANT_COUNT] = [
let plantstate: [PlantState; PLANT_COUNT] = [
PlantState::read_hardware_state(0, &mut board).await,
PlantState::read_hardware_state(1, &mut board).await,
PlantState::read_hardware_state(2, &mut board).await,
@@ -411,7 +409,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
PlantState::read_hardware_state(7, &mut board).await,
];
//publish_plant_states(&timezone_time.clone(), &plantstate).await;
publish_plant_states(&mut board, &timezone_time.clone(), &plantstate).await;
// let pump_required = plantstate
// .iter()
@@ -568,12 +566,12 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
info!("Lightstate is {:?}", light_state);
}
match serde_json::to_string(&light_state) {
match &serde_json::to_string(&light_state) {
Ok(state) => {
let _ = board
.board_hal
.get_esp()
.mqtt_publish("/light", state.as_bytes())
.mqtt_publish("/light", state)
.await;
}
Err(err) => {
@@ -587,25 +585,25 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
let _ = board
.board_hal
.get_esp()
.mqtt_publish("/deepsleep", "low Volt 12h".as_bytes()).await;
.mqtt_publish("/deepsleep", "low Volt 12h").await;
12 * 60
} else if is_day {
let _ = board
.board_hal
.get_esp()
.mqtt_publish("/deepsleep", "normal 20m".as_bytes()).await;
.mqtt_publish("/deepsleep", "normal 20m").await;
20
} else {
let _ = board
.board_hal
.get_esp()
.mqtt_publish("/deepsleep", "night 1h".as_bytes()).await;
.mqtt_publish("/deepsleep", "night 1h").await;
60
};
let _ = board
.board_hal
.get_esp()
.mqtt_publish("/state", "sleep".as_bytes())
.mqtt_publish("/state", "sleep")
.await;
info!("Go to sleep for {} minutes", deep_sleep_duration_minutes);
@@ -616,18 +614,17 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
//TODO
//mark_app_valid();
let stay_alive_mqtt = STAY_ALIVE.load(Ordering::Relaxed);
let stay_alive = stay_alive_mqtt;
let stay_alive = MQTT_STAY_ALIVE.load(Ordering::Relaxed);
info!("Check stay alive, current state is {}", stay_alive);
if stay_alive {
info!("Go to stay alive move");
let reboot_now = Arc::new(AtomicBool::new(false));
//TODO
//let _webserver = httpd(reboot_now.clone());
let _webserver = http_server(reboot_now.clone(), stack.take().unwrap());
wait_infinity(board, WaitType::MqttConfig, reboot_now.clone()).await;
} else {
//TODO wait for all mqtt publishes?
Timer::after_millis(5000).await;
board.board_hal.get_esp().set_restart_to_conf(false);
board
.board_hal
@@ -654,7 +651,6 @@ pub async fn do_secure_pump(
Timer::after_millis(10).await;
for step in 0..plant_config.pump_time_s as usize {
let flow_value = board.board_hal.get_tank_sensor()?.get_flow_meter_value();
let flow_value = 1;
flow_collector[step] = flow_value;
let flow_value_ml = flow_value as f32 * board.board_hal.get_config().tank.ml_per_pulse;
@@ -736,7 +732,7 @@ pub async fn do_secure_pump(
error = true;
break;
} else {
//eg v3 without a sensor ends here, do not spam
//e.g., v3 without a sensor ends here, do not spam
}
}
}
@@ -744,9 +740,8 @@ pub async fn do_secure_pump(
pump_time_s += 1;
}
}
board.board_hal.get_tank_sensor().unwrap().stop_flow_meter();
board.board_hal.get_tank_sensor()?.stop_flow_meter();
let final_flow_value = board.board_hal.get_tank_sensor()?.get_flow_meter_value();
let final_flow_value = 12;
let flow_value_ml = final_flow_value as f32 * board.board_hal.get_config().tank.ml_per_pulse;
info!(
"Final flow value is {} with {} ml",
@@ -764,42 +759,64 @@ pub async fn do_secure_pump(
})
}
async fn update_charge_indicator(board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'_>>) {
async fn update_charge_indicator(
board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>,
) {
//we have mppt controller, ask it for charging current
// let tank_state = determine_tank_state(&mut board);
//
// if tank_state.is_enabled() {
// 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(
// LogMessage::TankSensorMissing,
// raw_value_mv as u32,
// 0,
// "",
// "",
// ).await,
// TankError::SensorValueError { value, min, max } => log(
// LogMessage::TankSensorValueRangeError,
// min as u32,
// max as u32,
// &format!("{}", value),
// "",
// ).await,
// TankError::BoardError(err) => {
// log(LogMessage::TankSensorBoardError, 0, 0, "", &err.to_string()).await
// }
// }
// // disabled cannot trigger this because of wrapping if is_enabled
// board.board_hal.general_fault(true).await;
// } else if tank_state
// .warn_level(&board.board_hal.get_config().tank)
// .is_ok_and(|warn| warn)
// {
// log(LogMessage::TankWaterLevelLow, 0, 0, "", "").await;
// board.board_hal.general_fault(true).await;
// }
// }
let tank_state = determine_tank_state(board).await;
if tank_state.is_enabled() {
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_ACCESS
.lock()
.await
.log(
LogMessage::TankSensorMissing,
raw_value_mv as u32,
0,
"",
"",
)
.await
}
TankError::SensorValueError { value, min, max } => {
LOG_ACCESS
.lock()
.await
.log(
LogMessage::TankSensorValueRangeError,
min as u32,
max as u32,
&format!("{}", value),
"",
)
.await
}
TankError::BoardError(err) => {
LOG_ACCESS
.lock()
.await
.log(LogMessage::TankSensorBoardError, 0, 0, "", &err.to_string())
.await
}
}
// disabled cannot trigger this because of wrapping if is_enabled
board.board_hal.general_fault(true).await;
} else if tank_state
.warn_level(&board.board_hal.get_config().tank)
.is_ok_and(|warn| warn)
{
LOG_ACCESS
.lock()
.await
.log(LogMessage::TankWaterLevelLow, 0, 0, "", "")
.await;
board.board_hal.general_fault(true).await;
}
}
if let Ok(current) = board.board_hal.get_mptt_current().await {
let _ = board
.board_hal
@@ -818,48 +835,39 @@ async fn update_charge_indicator(board: &mut MutexGuard<'_, CriticalSectionRawMu
let _ = board.board_hal.set_charge_indicator(false);
}
}
//
// async fn publish_tank_state(tank_state: &TankState, water_temp: &anyhow::Result<f32>) {
// let board = &mut BOARD_ACCESS.get().lock().await;
// match serde_json::to_string(
// &tank_state.as_mqtt_info(&board.board_hal.get_config().tank, water_temp),
// ) {
// Ok(state) => {
// let _ = board
// .board_hal
// .get_esp()
// .mqtt_publish("/water", state.as_bytes());
// }
// Err(err) => {
// info!("Error publishing tankstate {}", err);
// }
// };
// }
// async fn publish_plant_states(timezone_time: &DateTime<Tz>, plantstate: &[PlantState; 8]) {
// let board = &mut BOARD_ACCESS.get().lock().await;
// for (plant_id, (plant_state, plant_conf)) in plantstate
// .iter()
// .zip(&board.board_hal.get_config().plants.clone())
// .enumerate()
// {
// match serde_json::to_string(&plant_state.to_mqtt_info(plant_conf, timezone_time)) {
// Ok(state) => {
// let plant_topic = format!("/plant{}", plant_id + 1);
// let _ = board
// .board_hal
// .get_esp()
// .mqtt_publish(&plant_topic, state.as_bytes())
// .await;
// //TODO? reduce speed as else messages will be dropped
// Timer::after_millis(200).await
// }
// Err(err) => {
// error!("Error publishing plant state {}", err);
// }
// };
// }
// }
async fn publish_tank_state(
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
tank_state: &TankState,
water_temp: FatResult<f32>,
) {
let state = serde_json::to_string(
&tank_state.as_mqtt_info(&board.board_hal.get_config().tank, &water_temp),
)
.unwrap();
let _ = board.board_hal.get_esp().mqtt_publish("/water", &*state);
}
async fn publish_plant_states(
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
timezone_time: &DateTime<Tz>,
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 _ = board
.board_hal
.get_esp()
.mqtt_publish(&plant_topic, &state)
.await;
}
}
async fn publish_firmware_info(
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
@@ -868,30 +876,33 @@ async fn publish_firmware_info(
timezone_time: &String,
) {
let esp = board.board_hal.get_esp();
let _ = esp.mqtt_publish("/firmware/address", ip_address).await;
let _ = esp
.mqtt_publish("/firmware/address", ip_address.as_bytes())
.mqtt_publish("/firmware/githash", &version.git_hash)
.await;
let _ = esp
.mqtt_publish("/firmware/githash", version.git_hash.as_bytes())
.mqtt_publish("/firmware/buildtime", &version.build_time)
.await;
let _ = esp
.mqtt_publish("/firmware/buildtime", version.build_time.as_bytes())
.await;
let _ = esp.mqtt_publish("/firmware/last_online", timezone_time.as_bytes());
let _ = esp.mqtt_publish("/firmware/last_online", timezone_time);
let state = esp.get_ota_state();
let _ = esp
.mqtt_publish("/firmware/ota_state", state.as_bytes())
.await;
let _ = esp.mqtt_publish("/firmware/ota_state", &state).await;
let slot = esp.get_ota_slot();
let _ = esp
.mqtt_publish("/firmware/ota_slot", format!("slot{slot}").as_bytes())
.mqtt_publish("/firmware/ota_slot", &format!("slot{slot}"))
.await;
let _ = esp.mqtt_publish("/state", "online".as_bytes()).await;
let _ = esp.mqtt_publish("/state", "online").await;
}
macro_rules! mk_static {
($t:ty,$val:expr) => {{
static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new();
#[deny(unused_attributes)]
let x = STATIC_CELL.uninit().write(($val));
x
}};
}
async fn try_connect_wifi_sntp_mqtt(
board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>,
mut stack_store: &mut OptionLock<Stack<'static>>,
stack_store: &mut OptionLock<Stack<'static>>,
) -> NetworkMode {
let nw_conf = &board.board_hal.get_config().network.clone();
match board.board_hal.get_esp().wifi(nw_conf).await {
@@ -917,8 +928,9 @@ async fn try_connect_wifi_sntp_mqtt(
};
let mqtt_connected = if board.board_hal.get_config().network.mqtt_url.is_some() {
let nw_config = &board.board_hal.get_config().network.clone();
match board.board_hal.get_esp().mqtt(nw_config).await {
let nw_config = board.board_hal.get_config().network.clone();
let nw_config = mk_static!(NetworkConfig, nw_config);
match board.board_hal.get_esp().mqtt(nw_config, stack).await {
Ok(_) => {
info!("Mqtt connection ready");
true
@@ -957,25 +969,23 @@ async fn pump_info(
let pump_info = PumpInfo {
enabled: pump_active,
pump_ineffective,
median_current_ma: median_current_ma,
max_current_ma: max_current_ma,
min_current_ma: min_current_ma,
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 _ = BOARD_ACCESS
BOARD_ACCESS
.get()
.await
.lock()
.await
.board_hal
.get_esp()
.mqtt_publish(&pump_topic, state.as_bytes());
//reduce speed as else messages will be dropped
//TODO maybee not required for low level hal?
Timer::after_millis(200).await;
.mqtt_publish(&pump_topic, &state)
.await;
}
Err(err) => {
warn!("Error publishing pump state {}", err);
@@ -992,9 +1002,7 @@ async fn publish_mppt_state(
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).map(|s| s.into_bytes())
{
if let Ok(serialized_solar_state_bytes) = serde_json::to_string(&solar_state) {
let _ = board
.board_hal
.get_esp()
@@ -1014,9 +1022,9 @@ async fn publish_battery_state(
let value = match state {
Ok(state) => {
let json = serde_json::to_string(&state).unwrap().to_owned();
json.as_bytes().to_owned()
json.to_owned()
}
Err(_) => "error".as_bytes().to_owned(),
Err(_) => "error".to_owned(),
};
{
let _ = board
@@ -1083,7 +1091,7 @@ async fn wait_infinity(
Timer::after_millis(delay).await;
if wait_type == WaitType::MqttConfig && !STAY_ALIVE.load(Ordering::Relaxed) {
if wait_type == WaitType::MqttConfig && !MQTT_STAY_ALIVE.load(Ordering::Relaxed) {
reboot_now.store(true, Ordering::Relaxed);
}
if reboot_now.load(Ordering::Relaxed) {
@@ -1107,7 +1115,7 @@ async fn main(spawner: Spawner) -> ! {
logger::init_logger_from_env();
//force init here!
println!("Hal init");
match BOARD_ACCESS.init(PlantHal::create(spawner).await.unwrap()) {
match BOARD_ACCESS.init(PlantHal::create().await.unwrap()) {
Ok(_) => {}
Err(_) => {
panic!("Could not set hal to static")