get most stuff working again, by upgrading to newer esp-hal version

this involved adding a lot of code from the develop branch step by step
there are still some bugs, but at least i can get into the web interface
and configure stuff again \o/ … measuring and pumping is working as well
This commit is contained in:
2026-05-04 23:46:27 +02:00
parent ecb7707357
commit db401aac55
23 changed files with 2029 additions and 1292 deletions

View File

@@ -11,20 +11,22 @@
//TODO insert version here and read it in other parts, also read this for the ota webview
esp_bootloader_esp_idf::esp_app_desc!();
use alloc::vec::Vec;
use config::PlantControllerConfig;
use esp_backtrace as _;
use hal::PROGRESS_ACTIVE;
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::log::log;
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::{info, warn};
use ::log::{info, warn, error};
use alloc::borrow::ToOwned;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
@@ -37,7 +39,7 @@ use embassy_net::Stack;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::mutex::{Mutex, MutexGuard};
use embassy_sync::once_lock::OnceLock;
use embassy_time::Timer;
use embassy_time::{Duration, Instant, Timer};
use esp_hal::rom::ets_delay_us;
use esp_hal::system::software_reset;
use esp_println::{logger, println};
@@ -165,35 +167,28 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
let cur = match board.board_hal.get_rtc_module().get_rtc_time().await {
Ok(value) => {
{
let guard = TIME_ACCESS.get().await.lock().await;
guard.set_current_time_us(value.timestamp_micros() as u64);
board.board_hal.set_time(&value.fixed_offset()).await;
}
value
}
Err(err) => {
info!("rtc module error: {:?}", err);
board.board_hal.general_fault(true).await;
esp_time().await
board.board_hal.get_time().await
}
};
info!("Step 1");
//check if we know the time current > 2020 (plausibility checks, this code is newer than 2020)
if cur.year() < 2020 {
to_config = true;
LOG_ACCESS
.lock()
.await
.log(LogMessage::YearInplausibleForceConfig, 0, 0, "", "")
.await;
log(LogMessage::YearInplausibleForceConfig, 0, 0, "", "");
}
info!("cur is {}", cur);
update_charge_indicator(&mut board).await;
if board.board_hal.get_esp().get_restart_to_conf() {
LOG_ACCESS
.lock()
.await
.log(LogMessage::ConfigModeSoftwareOverride, 0, 0, "", "")
.await;
log(LogMessage::ConfigModeSoftwareOverride, 0, 0, "", "");
for _i in 0..2 {
board.board_hal.general_fault(true).await;
Timer::after_millis(100).await;
@@ -205,11 +200,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
board.board_hal.get_esp().set_restart_to_conf(false);
} else if board.board_hal.get_esp().mode_override_pressed() {
board.board_hal.general_fault(true).await;
LOG_ACCESS
.lock()
.await
.log(LogMessage::ConfigModeButtonOverride, 0, 0, "", "")
.await;
log(LogMessage::ConfigModeButtonOverride, 0, 0, "", "");
for _i in 0..5 {
board.board_hal.general_fault(true).await;
Timer::after_millis(100).await;
@@ -232,18 +223,18 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
{
info!("No wifi configured, starting initial config mode");
let stack = board.board_hal.get_esp().wifi_ap().await?;
let stack = board.board_hal.get_esp().wifi_ap(spawner).await?;
let reboot_now = Arc::new(AtomicBool::new(false));
println!("starting webserver");
spawner.spawn(http_server(reboot_now.clone(), stack))?;
wait_infinity(board, WaitType::MissingConfig, reboot_now.clone()).await;
spawner.spawn(http_server(reboot_now.clone(), stack)?);
wait_infinity(board, WaitType::MissingConfig, reboot_now.clone(), UTC).await;
}
let mut stack: OptionLock<Stack> = OptionLock::empty();
let network_mode = if board.board_hal.get_config().network.ssid.is_some() {
try_connect_wifi_sntp_mqtt(&mut board, &mut stack).await
try_connect_wifi_sntp_mqtt(&mut board, &mut stack, spawner).await
} else {
info!("No wifi configured");
//the current sensors require this amount to stabilize, in the case of Wi-Fi this is already handled due to connect timings;
@@ -256,7 +247,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
let res = {
let esp = board.board_hal.get_esp();
esp.wifi_ap().await
esp.wifi_ap(spawner).await
};
match res {
Ok(ap_stack) => {
@@ -291,39 +282,36 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
let _ = publish_mppt_state(&mut board).await;
}
LOG_ACCESS
.lock()
.await
.log(
LogMessage::StartupInfo,
matches!(network_mode, NetworkMode::WIFI { .. }) as u32,
matches!(
network_mode,
NetworkMode::WIFI {
sntp: SntpMode::SYNC { .. },
..
}
) as u32,
matches!(network_mode, NetworkMode::WIFI { mqtt: true, .. })
.to_string()
.as_str(),
"",
)
.await;
log(
LogMessage::StartupInfo,
matches!(network_mode, NetworkMode::WIFI { .. }) as u32,
matches!(
network_mode,
NetworkMode::WIFI {
sntp: SntpMode::SYNC { .. },
..
}
) as u32,
matches!(network_mode, NetworkMode::WIFI { mqtt: true, .. })
.to_string()
.as_str(),
"",
);
if to_config {
//check if client or ap mode and init Wi-Fi
info!("executing config mode override");
//config upload will trigger reboot!
let reboot_now = Arc::new(AtomicBool::new(false));
spawner.spawn(http_server(reboot_now.clone(), stack.take().unwrap()))?;
wait_infinity(board, WaitType::ConfigButton, reboot_now.clone()).await;
let stack_val = stack.take();
if let Some(s) = stack_val {
spawner.spawn(http_server(reboot_now.clone(), s)?);
} else {
bail!("Network stack missing, hard abort")
}
wait_infinity(board, WaitType::ConfigButton, reboot_now.clone(), UTC).await;
} else {
LOG_ACCESS
.lock()
.await
.log(LogMessage::NormalRun, 0, 0, "", "")
.await;
log(LogMessage::NormalRun, 0, 0, "", "");
}
let _dry_run = false;
@@ -334,38 +322,22 @@ 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_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::SensorMissing(raw_value_mv) => log(
LogMessage::TankSensorMissing,
raw_value_mv as u32,
0,
"",
"",
),
TankError::SensorValueError { value, min, max } => log(
LogMessage::TankSensorValueRangeError,
min as u32,
max as u32,
&format!("{value}"),
"",
),
TankError::BoardError(err) => {
LOG_ACCESS
.lock()
.await
.log(LogMessage::TankSensorBoardError, 0, 0, "", &err.to_string())
.await
log(LogMessage::TankSensorBoardError, 0, 0, "", &err.to_string())
}
}
// disabled cannot trigger this because of wrapping if is_enabled
@@ -374,11 +346,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
.warn_level(&board.board_hal.get_config().tank)
.is_ok_and(|warn| warn)
{
LOG_ACCESS
.lock()
.await
.log(LogMessage::TankWaterLevelLow, 0, 0, "", "")
.await;
log(LogMessage::TankWaterLevelLow, 0, 0, "", "");
board.board_hal.general_fault(true).await;
}
}
@@ -620,7 +588,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
if stay_alive {
let reboot_now = Arc::new(AtomicBool::new(false));
let _webserver = http_server(reboot_now.clone(), stack.take().unwrap());
wait_infinity(board, WaitType::MqttConfig, reboot_now.clone()).await;
wait_infinity(board, WaitType::MqttConfig, reboot_now.clone(), UTC).await;
} else {
//TODO wait for all mqtt publishes?
Timer::after_millis(5000).await;
@@ -671,17 +639,13 @@ pub async fn do_secure_pump(
let high_current = current_ma > plant_config.max_pump_current_ma;
if high_current {
if first_error {
LOG_ACCESS
.lock()
.await
.log(
LogMessage::PumpOverCurrent,
plant_id as u32 + 1,
current_ma as u32,
plant_config.max_pump_current_ma.to_string().as_str(),
step.to_string().as_str(),
)
.await;
log(
LogMessage::PumpOverCurrent,
plant_id as u32 + 1,
current_ma as u32,
plant_config.max_pump_current_ma.to_string().as_str(),
step.to_string().as_str(),
);
board.board_hal.general_fault(true).await;
board.board_hal.fault(plant_id, true).await?;
if !plant_config.ignore_current_error {
@@ -694,17 +658,13 @@ pub async fn do_secure_pump(
let low_current = current_ma < plant_config.min_pump_current_ma;
if low_current {
if first_error {
LOG_ACCESS
.lock()
.await
.log(
LogMessage::PumpOpenLoopCurrent,
plant_id as u32 + 1,
current_ma as u32,
plant_config.min_pump_current_ma.to_string().as_str(),
step.to_string().as_str(),
)
.await;
log(
LogMessage::PumpOpenLoopCurrent,
plant_id as u32 + 1,
current_ma as u32,
plant_config.min_pump_current_ma.to_string().as_str(),
step.to_string().as_str(),
);
board.board_hal.general_fault(true).await;
board.board_hal.fault(plant_id, true).await?;
if !plant_config.ignore_current_error {
@@ -718,17 +678,13 @@ pub async fn do_secure_pump(
Err(err) => {
if !plant_config.ignore_current_error {
info!("Error getting pump current: {}", err);
LOG_ACCESS
.lock()
.await
.log(
LogMessage::PumpMissingSensorCurrent,
plant_id as u32,
0,
"",
"",
)
.await;
log(
LogMessage::PumpMissingSensorCurrent,
plant_id as u32,
0,
"",
"",
);
error = true;
break;
} else {
@@ -761,25 +717,15 @@ pub async fn do_secure_pump(
async fn update_charge_indicator(
board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>,
) {
) -> FatResult<()> {
//FIXME add config and code to allow power supply mode, in this case this is a nop
//we have mppt controller, ask it for charging current
if let Ok(current) = board.board_hal.get_mptt_current().await {
let _ = board
.board_hal
.set_charge_indicator(current.as_milliamperes() > 20_f64);
}
//fallback to battery controller and ask it instead
else if let Ok(charging) = board
let current = board.board_hal.get_mptt_current().await?;
board
.board_hal
.get_battery_monitor()
.average_current_milli_ampere()
.await
{
let _ = board.board_hal.set_charge_indicator(charging > 20);
} else {
//who knows
let _ = board.board_hal.set_charge_indicator(false);
}
.set_charge_indicator(current.as_milliamperes() > 20_f64)
.await?;
Ok(())
}
async fn publish_tank_state(
@@ -818,25 +764,16 @@ async fn publish_plant_states(
async fn publish_firmware_info(
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
version: VersionInfo,
ip_address: &String,
timezone_time: &String,
ip_address: &str,
timezone_time: &str,
) {
let esp = board.board_hal.get_esp();
let _ = esp.mqtt_publish("/firmware/address", ip_address).await;
let _ = esp
.mqtt_publish("/firmware/githash", &version.git_hash)
esp.mqtt_publish("/firmware/address", ip_address).await;
esp.mqtt_publish("/firmware/state", format!("{:?}", &version).as_str())
.await;
let _ = esp
.mqtt_publish("/firmware/buildtime", &version.build_time)
esp.mqtt_publish("/firmware/last_online", timezone_time)
.await;
let _ = esp.mqtt_publish("/firmware/last_online", timezone_time);
let state = esp.get_ota_state();
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}"))
.await;
let _ = esp.mqtt_publish("/state", "online").await;
esp.mqtt_publish("/state", "online").await;
}
macro_rules! mk_static {
($t:ty,$val:expr) => {{
@@ -849,25 +786,25 @@ macro_rules! mk_static {
async fn try_connect_wifi_sntp_mqtt(
board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>,
stack_store: &mut OptionLock<Stack<'static>>,
spawner: Spawner,
) -> NetworkMode {
let nw_conf = &board.board_hal.get_config().network.clone();
match board.board_hal.get_esp().wifi(nw_conf).await {
match board.board_hal.get_esp().wifi(nw_conf, spawner).await {
Ok(stack) => {
stack_store.replace(stack);
let sntp_mode: SntpMode = match board
.board_hal
.get_esp()
.sntp(1000 * 10, stack.clone())
.await
{
let sntp_mode: SntpMode = match board.board_hal.get_esp().sntp(1000 * 10, stack).await {
Ok(new_time) => {
info!("Using time from sntp {}", new_time.to_rfc3339());
let _ = board.board_hal.get_rtc_module().set_rtc_time(&new_time);
let _ = board
.board_hal
.get_rtc_module()
.set_rtc_time(&new_time)
.await;
SntpMode::SYNC { current: new_time }
}
Err(err) => {
warn!("sntp error: {}", err);
warn!("sntp error: {err}");
board.board_hal.general_fault(true).await;
SntpMode::OFFLINE
}
@@ -876,27 +813,40 @@ 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();
let nw_config = mk_static!(NetworkConfig, nw_config);
match board.board_hal.get_esp().mqtt(nw_config, stack).await {
match board
.board_hal
.get_esp()
.mqtt(nw_config, stack, spawner)
.await
{
Ok(_) => {
info!("Mqtt connection ready");
true
}
Err(err) => {
warn!("Could not connect mqtt due to {}", err);
warn!("Could not connect mqtt due to {err}");
false
}
}
} else {
false
};
let ip = match stack.config_v4() {
Some(config) => config.address.address().to_string(),
None => match stack.config_v6() {
Some(config) => config.address.address().to_string(),
None => String::from("No IP"),
},
};
NetworkMode::WIFI {
sntp: sntp_mode,
mqtt: mqtt_connected,
ip_address: stack.hardware_address().to_string(),
ip_address: ip,
}
}
Err(err) => {
info!("Offline mode due to {}", err);
info!("Offline mode due to {err}");
board.board_hal.general_fault(true).await;
NetworkMode::OFFLINE
}
@@ -985,6 +935,7 @@ async fn wait_infinity(
board: MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
wait_type: WaitType,
reboot_now: Arc<AtomicBool>,
timezone: Tz,
) -> ! {
//since we force to have the lock when entering, we can release it to ensure the caller does not forget to dispose of it
drop(board);
@@ -992,55 +943,155 @@ async fn wait_infinity(
let delay = wait_type.blink_pattern();
let mut led_count = 8;
let mut pattern_step = 0;
let serial_config_receive = AtomicBool::new(false);
let mut suppress_further_mppt_error = false;
let mut last_mqtt_update: Option<Instant> = None;
// Long-press exit (for webserver config modes): hold boot button for 5 seconds.
let mut exit_hold_started: Option<Instant> = None;
let exit_hold_duration = Duration::from_secs(5);
let mut exit_hold_blink = false;
loop {
// While in config webserver mode, allow exiting via long-press.
if matches!(wait_type, WaitType::MissingConfig | WaitType::ConfigButton) {
let mut board = BOARD_ACCESS.get().await.lock().await;
let pressed = board.board_hal.get_esp().mode_override_pressed();
match (pressed, exit_hold_started) {
(true, None) => {
exit_hold_started = Some(Instant::now());
PROGRESS_ACTIVE.store(true, Ordering::Relaxed);
}
(false, Some(_)) => {
exit_hold_started = None;
// Clear any interim hold display.
board.board_hal.clear_progress().await;
}
_ => {}
}
if let Some(started) = exit_hold_started {
let elapsed = Instant::now() - started;
// Visible countdown: fill LEDs progressively during the hold.
// Also toggle general fault LED to match the "enter config" visibility.
exit_hold_blink = !exit_hold_blink;
let progress = core::cmp::min(elapsed, exit_hold_duration);
let lit = ((progress.as_millis() * 8) / exit_hold_duration.as_millis())
.saturating_add(1)
.min(8) as usize;
for i in 0..8 {
let _ = board.board_hal.fault(i, i < lit).await;
}
board.board_hal.general_fault(exit_hold_blink).await;
if elapsed >= exit_hold_duration {
info!("Exiting config mode due to 5s button hold");
board.board_hal.get_esp().set_restart_to_conf(false);
// ensure clean http answer / visible confirmation
Timer::after_millis(500).await;
board.board_hal.deep_sleep(0).await;
}
// Short tick while holding so the pattern updates smoothly.
drop(board);
Timer::after_millis(100).await;
continue;
}
// Release lock and continue with normal wait blinking.
drop(board);
}
{
let mut board = BOARD_ACCESS.get().await.lock().await;
update_charge_indicator(&mut board).await;
match update_charge_indicator(&mut board).await {
Ok(_) => {}
Err(error) => {
if !suppress_further_mppt_error {
error!("Error updating charge indicator: {error}");
suppress_further_mppt_error = true;
}
}
};
match wait_type {
WaitType::MissingConfig => {
// Keep existing behavior: circular filling pattern
led_count %= 8;
led_count += 1;
for i in 0..8 {
let _ = board.board_hal.fault(i, i < led_count);
}
}
WaitType::ConfigButton => {
// Alternating pattern: 1010 1010 -> 0101 0101
pattern_step = (pattern_step + 1) % 2;
for i in 0..8 {
let _ = board.board_hal.fault(i, (i + pattern_step) % 2 == 0);
}
}
WaitType::MqttConfig => {
// Moving dot pattern
pattern_step = (pattern_step + 1) % 8;
for i in 0..8 {
let _ = board.board_hal.fault(i, i == pattern_step);
}
match handle_serial_config(&mut board, &serial_config_receive, &reboot_now).await {
Ok(_) => {}
Err(e) => {
error!("Error handling serial config: {e}");
}
}
board.board_hal.general_fault(true).await;
// MQTT updates in config mode
let now = Instant::now();
if last_mqtt_update.is_none()
|| now.duration_since(last_mqtt_update.unwrap_or(Instant::from_secs(0)))
>= Duration::from_secs(60)
{
let cur = board.board_hal.get_time().await;
let timezone_time = cur.with_timezone(&timezone);
let esp = board.board_hal.get_esp();
esp.mqtt_publish("/state", "config").await;
esp.mqtt_publish("/firmware/last_online", &timezone_time.to_rfc3339())
.await;
last_mqtt_update = Some(now);
}
// Skip default blink code when a progress display is active
if !PROGRESS_ACTIVE.load(Ordering::Relaxed) {
match wait_type {
WaitType::MissingConfig => {
// Keep existing behavior: circular filling pattern
led_count %= 8;
led_count += 1;
for i in 0..8 {
let _ = board.board_hal.fault(i, i < led_count).await;
}
}
WaitType::ConfigButton => {
// Alternating pattern: 1010 1010 -> 0101 0101
pattern_step = (pattern_step + 1) % 2;
for i in 0..8 {
let _ = board.board_hal.fault(i, (i + pattern_step) % 2 == 0).await;
}
}
WaitType::MqttConfig => {
// Moving dot pattern
pattern_step = (pattern_step + 1) % 8;
for i in 0..8 {
let _ = board.board_hal.fault(i, i == pattern_step).await;
}
}
}
board.board_hal.general_fault(true).await;
}
}
Timer::after_millis(delay).await;
{
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.general_fault(false).await;
// Clear all LEDs
for i in 0..8 {
let _ = board.board_hal.fault(i, false);
// Skip clearing LEDs when progress is active to avoid interrupting the progress display
if !PROGRESS_ACTIVE.load(Ordering::Relaxed) {
board.board_hal.general_fault(false).await;
// Clear all LEDs
for i in 0..8 {
let _ = board.board_hal.fault(i, false).await;
}
}
}
Timer::after_millis(delay).await;
hal::PlantHal::feed_watchdog();
if wait_type == WaitType::MqttConfig && !MQTT_STAY_ALIVE.load(Ordering::Relaxed) {
reboot_now.store(true, Ordering::Relaxed);
}
if reboot_now.load(Ordering::Relaxed) {
info!("Rebooting now");
//ensure clean http answer
Timer::after_millis(500).await;
BOARD_ACCESS
@@ -1055,7 +1106,47 @@ async fn wait_infinity(
}
}
#[esp_hal_embassy::main]
async fn handle_serial_config(
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'_>>,
serial_config_receive: &AtomicBool,
reboot_now: &AtomicBool,
) -> FatResult<()> {
match board.board_hal.get_esp().read_serial_line().await {
Ok(serial_line) => match serial_line {
None => Ok(()),
Some(line) => {
if serial_config_receive.load(Ordering::Relaxed) {
let ll = line.as_str();
let config: PlantControllerConfig = serde_json::from_str(ll)?;
board
.board_hal
.get_esp()
.save_config(Vec::from(ll.as_bytes()))
.await?;
board.board_hal.set_config(config);
serial_config_receive.store(false, Ordering::Relaxed);
info!("Config received, rebooting");
board.board_hal.get_esp().set_restart_to_conf(false);
reboot_now.store(true, Ordering::Relaxed);
Ok(())
} else {
if line == "automation:streamconfig" {
serial_config_receive.store(true, Ordering::Relaxed);
info!("streamconfig:recieving");
}
Ok(())
}
}
},
Err(_) => {
error!("Error reading serial line");
Ok(())
}
}
}
use embassy_time::WithTimeout;
#[esp_rtos::main]
async fn main(spawner: Spawner) -> ! {
// intialize embassy
logger::init_logger_from_env();
@@ -1099,11 +1190,17 @@ async fn get_version(
let hash = &env!("VERGEN_GIT_SHA")[0..8];
let board = board.board_hal.get_esp();
let ota_slot = board.get_ota_slot();
let heap = esp_alloc::HEAP.stats();
VersionInfo {
git_hash: branch + "@" + hash,
build_time: env!("VERGEN_BUILD_TIMESTAMP").to_owned(),
partition: ota_slot,
current: format!("{:?}", board.current),
slot0_state: format!("{:?}", board.slot0_state),
slot1_state: format!("{:?}", board.slot1_state),
heap_total: heap.size,
heap_used: heap.current_usage,
heap_free: heap.size.saturating_sub(heap.current_usage),
heap_max_used: heap.max_usage,
}
}
@@ -1111,5 +1208,11 @@ async fn get_version(
struct VersionInfo {
git_hash: String,
build_time: String,
partition: String,
current: String,
slot0_state: String,
slot1_state: String,
heap_total: usize,
heap_used: usize,
heap_free: usize,
heap_max_used: usize,
}