Files
PlantCtrl/rust/src/hal/mod.rs
2025-10-07 21:50:33 +02:00

688 lines
24 KiB
Rust

pub(crate) mod battery;
mod can_api;
pub mod esp;
mod initial_hal;
mod little_fs2storage_adapter;
pub(crate) mod rtc;
mod v3_hal;
mod v3_shift_register;
mod v4_hal;
mod v4_sensor;
mod water;
use crate::alloc::string::ToString;
use crate::hal::rtc::{DS3231Module, RTCModuleInteraction};
use esp_hal::peripherals::Peripherals;
use esp_hal::peripherals::ADC1;
use esp_hal::peripherals::GPIO0;
use esp_hal::peripherals::GPIO10;
use esp_hal::peripherals::GPIO11;
use esp_hal::peripherals::GPIO12;
use esp_hal::peripherals::GPIO13;
use esp_hal::peripherals::GPIO14;
use esp_hal::peripherals::GPIO15;
use esp_hal::peripherals::GPIO16;
use esp_hal::peripherals::GPIO17;
use esp_hal::peripherals::GPIO18;
use esp_hal::peripherals::GPIO2;
use esp_hal::peripherals::GPIO21;
use esp_hal::peripherals::GPIO22;
use esp_hal::peripherals::GPIO23;
use esp_hal::peripherals::GPIO24;
use esp_hal::peripherals::GPIO25;
use esp_hal::peripherals::GPIO26;
use esp_hal::peripherals::GPIO27;
use esp_hal::peripherals::GPIO28;
use esp_hal::peripherals::GPIO29;
use esp_hal::peripherals::GPIO3;
use esp_hal::peripherals::GPIO30;
use esp_hal::peripherals::GPIO4;
use esp_hal::peripherals::GPIO5;
use esp_hal::peripherals::GPIO6;
use esp_hal::peripherals::GPIO7;
use esp_hal::peripherals::GPIO8;
use esp_hal::peripherals::TWAI0;
use crate::{
bail,
config::{BatteryBoardVersion, BoardVersion, PlantControllerConfig},
hal::{
battery::{BatteryInteraction, NoBatteryMonitor},
esp::Esp,
},
log::LogMessage,
BOARD_ACCESS,
};
use alloc::boxed::Box;
use alloc::format;
use alloc::sync::Arc;
use async_trait::async_trait;
use bincode::{Decode, Encode};
use bq34z100::Bq34z100g1Driver;
use chrono::{DateTime, FixedOffset, Utc};
use core::cell::RefCell;
use ds323x::ic::DS3231;
use ds323x::interface::I2cInterface;
use ds323x::{DateTimeAccess, Ds323x};
use eeprom24x::addr_size::TwoBytes;
use eeprom24x::page_size::B32;
use eeprom24x::unique_serial::No;
use eeprom24x::{Eeprom24x, SlaveAddr, Storage};
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::blocking_mutex::CriticalSectionMutex;
use esp_bootloader_esp_idf::partitions::{
AppPartitionSubType, DataPartitionSubType, FlashRegion, PartitionEntry,
};
use esp_hal::clock::CpuClock;
use esp_hal::gpio::{Input, InputConfig, Pull};
use measurements::{Current, Voltage};
use crate::fat_error::{ContextExt, FatError, FatResult};
use crate::hal::battery::{print_battery_bq34z100, BQ34Z100G1};
use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem;
use crate::hal::water::TankSensor;
use crate::log::LOG_ACCESS;
use embassy_sync::mutex::Mutex;
use embassy_sync::once_lock::OnceLock;
use embedded_storage::nor_flash::ReadNorFlash;
use esp_alloc as _;
use esp_backtrace as _;
use esp_bootloader_esp_idf::ota::{Ota, OtaImageState};
use esp_bootloader_esp_idf::ota::{Slot as ota_slot, Slot};
use esp_hal::delay::Delay;
use esp_hal::i2c::master::{BusTimeout, Config, I2c};
use esp_hal::pcnt::unit::Unit;
use esp_hal::pcnt::Pcnt;
use esp_hal::rng::Rng;
use esp_hal::rtc_cntl::{Rtc, SocResetReason};
use esp_hal::system::reset_reason;
use esp_hal::time::Rate;
use esp_hal::timer::timg::TimerGroup;
use esp_hal::Blocking;
use esp_storage::FlashStorage;
use esp_wifi::{init, EspWifiController};
use littlefs2::fs::{Allocation, Filesystem as lfs2Filesystem};
use littlefs2::object_safe::DynStorage;
use log::{error, info, warn};
use portable_atomic::AtomicBool;
pub static TIME_ACCESS: OnceLock<Mutex<CriticalSectionRawMutex, Rtc>> = OnceLock::new();
//Only support for 8 right now!
pub const PLANT_COUNT: usize = 8;
pub static PROGRESS_ACTIVE: AtomicBool = AtomicBool::new(false);
const TANK_MULTI_SAMPLE: usize = 11;
pub static I2C_DRIVER: OnceLock<
embassy_sync::blocking_mutex::Mutex<CriticalSectionRawMutex, RefCell<I2c<Blocking>>>,
> = OnceLock::new();
#[derive(Debug, PartialEq, Clone, Copy, Encode, Decode)]
pub enum Sensor {
A,
B,
}
pub struct PlantHal {}
pub struct HAL<'a> {
pub board_hal: Box<dyn BoardInteraction<'a> + Send>,
}
#[async_trait]
pub trait BoardInteraction<'a> {
fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError>;
fn get_esp(&mut self) -> &mut Esp<'a>;
fn get_config(&mut self) -> &PlantControllerConfig;
fn get_battery_monitor(&mut self) -> &mut Box<dyn BatteryInteraction + Send>;
fn get_rtc_module(&mut self) -> &mut Box<dyn RTCModuleInteraction + Send>;
async fn set_charge_indicator(&mut self, charging: bool) -> Result<(), FatError>;
async fn deep_sleep(&mut self, duration_in_ms: u64) -> !;
fn is_day(&self) -> bool;
//should be multsampled
async fn light(&mut self, enable: bool) -> Result<(), FatError>;
async fn pump(&mut self, plant: usize, enable: bool) -> Result<(), FatError>;
async fn pump_current(&mut self, plant: usize) -> Result<Current, FatError>;
async fn fault(&mut self, plant: usize, enable: bool) -> Result<(), FatError>;
async fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result<f32, FatError>;
async fn general_fault(&mut self, enable: bool);
async fn test(&mut self) -> Result<(), FatError>;
fn set_config(&mut self, config: PlantControllerConfig);
async fn get_mptt_voltage(&mut self) -> Result<Voltage, FatError>;
async fn get_mptt_current(&mut self) -> Result<Current, FatError>;
// Return JSON string with autodetected sensors per plant. Default: not supported.
async fn detect_sensors(&mut self) -> Result<alloc::string::String, FatError> {
bail!("Autodetection is only available on v4 HAL with CAN bus");
}
async fn progress(&mut self, counter: u32) {
// Indicate progress is active to suppress default wait_infinity blinking
crate::hal::PROGRESS_ACTIVE.store(true, core::sync::atomic::Ordering::Relaxed);
let current = counter % PLANT_COUNT as u32;
for led in 0..PLANT_COUNT {
if let Err(err) = self.fault(led, current == led as u32).await {
warn!("Fault on plant {}: {:?}", led, err);
}
}
let even = counter % 2 == 0;
let _ = self.general_fault(even.into()).await;
}
async fn clear_progress(&mut self) {
for led in 0..PLANT_COUNT {
if let Err(err) = self.fault(led, false).await {
warn!("Fault on plant {}: {:?}", led, err);
}
}
let _ = self.general_fault(false).await;
// Reset progress active flag so wait_infinity can resume blinking
crate::hal::PROGRESS_ACTIVE.store(false, core::sync::atomic::Ordering::Relaxed);
}
}
#[allow(dead_code)]
pub struct FreePeripherals<'a> {
pub gpio0: GPIO0<'a>,
pub gpio2: GPIO2<'a>,
pub gpio3: GPIO3<'a>,
pub gpio4: GPIO4<'a>,
pub gpio5: GPIO5<'a>,
pub gpio6: GPIO6<'a>,
pub gpio7: GPIO7<'a>,
pub gpio8: GPIO8<'a>,
// //config button here
pub gpio10: GPIO10<'a>,
pub gpio11: GPIO11<'a>,
pub gpio12: GPIO12<'a>,
pub gpio13: GPIO13<'a>,
pub gpio14: GPIO14<'a>,
pub gpio15: GPIO15<'a>,
pub gpio16: GPIO16<'a>,
pub gpio17: GPIO17<'a>,
pub gpio18: GPIO18<'a>,
// //i2c here
pub gpio21: GPIO21<'a>,
pub gpio22: GPIO22<'a>,
pub gpio23: GPIO23<'a>,
pub gpio24: GPIO24<'a>,
pub gpio25: GPIO25<'a>,
pub gpio26: GPIO26<'a>,
pub gpio27: GPIO27<'a>,
pub gpio28: GPIO28<'a>,
pub gpio29: GPIO29<'a>,
pub gpio30: GPIO30<'a>,
pub twai: TWAI0<'a>,
pub pcnt0: Unit<'a, 0>,
pub pcnt1: Unit<'a, 1>,
pub adc1: ADC1<'a>,
}
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
}};
}
impl PlantHal {
pub async fn create() -> Result<Mutex<CriticalSectionRawMutex, HAL<'static>>, FatError> {
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals: Peripherals = esp_hal::init(config);
esp_alloc::heap_allocator!(size: 64 * 1024);
esp_alloc::heap_allocator!(#[link_section = ".dram2_uninit"] size: 64000);
let rtc: Rtc = Rtc::new(peripherals.LPWR);
TIME_ACCESS
.init(Mutex::new(rtc))
.map_err(|_| FatError::String {
error: "Init error rct".to_string(),
})?;
let systimer = SystemTimer::new(peripherals.SYSTIMER);
let boot_button = Input::new(
peripherals.GPIO9,
InputConfig::default().with_pull(Pull::None),
);
// Reserve GPIO1 for deep sleep wake (configured just before entering sleep)
let wake_gpio1 = peripherals.GPIO1;
let rng = Rng::new(peripherals.RNG);
let timg0 = TimerGroup::new(peripherals.TIMG0);
let esp_wifi_ctrl = &*mk_static!(
EspWifiController<'static>,
init(timg0.timer0, rng.clone()).expect("Could not init wifi controller")
);
let (controller, interfaces) =
esp_wifi::wifi::new(&esp_wifi_ctrl, peripherals.WIFI).expect("Could not init wifi");
use esp_hal::timer::systimer::SystemTimer;
esp_hal_embassy::init(systimer.alarm0);
//let mut adc1 = Adc::new(peripherals.ADC1, adc1_config);
//
let pcnt_module = Pcnt::new(peripherals.PCNT);
let free_pins = FreePeripherals {
// can: peripherals.can,
// adc1: peripherals.adc1,
// pcnt0: peripherals.pcnt0,
// pcnt1: peripherals.pcnt1,
gpio0: peripherals.GPIO0,
gpio2: peripherals.GPIO2,
gpio3: peripherals.GPIO3,
gpio4: peripherals.GPIO4,
gpio5: peripherals.GPIO5,
gpio6: peripherals.GPIO6,
gpio7: peripherals.GPIO7,
gpio8: peripherals.GPIO8,
gpio10: peripherals.GPIO10,
gpio11: peripherals.GPIO11,
gpio12: peripherals.GPIO12,
gpio13: peripherals.GPIO13,
gpio14: peripherals.GPIO14,
gpio15: peripherals.GPIO15,
gpio16: peripherals.GPIO16,
gpio17: peripherals.GPIO17,
gpio18: peripherals.GPIO18,
gpio21: peripherals.GPIO21,
gpio22: peripherals.GPIO22,
gpio23: peripherals.GPIO23,
gpio24: peripherals.GPIO24,
gpio25: peripherals.GPIO25,
gpio26: peripherals.GPIO26,
gpio27: peripherals.GPIO27,
gpio28: peripherals.GPIO28,
gpio29: peripherals.GPIO29,
gpio30: peripherals.GPIO30,
twai: peripherals.TWAI0,
pcnt0: pcnt_module.unit0,
pcnt1: pcnt_module.unit1,
adc1: peripherals.ADC1,
};
let tablebuffer = mk_static!(
[u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN],
[0u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN]
);
let storage_ota = mk_static!(FlashStorage, FlashStorage::new());
let pt =
esp_bootloader_esp_idf::partitions::read_partition_table(storage_ota, tablebuffer)?;
let ota_data = mk_static!(
PartitionEntry,
pt.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data(
DataPartitionSubType::Ota,
))?
.expect("No OTA data partition found")
);
let ota_data = mk_static!(
FlashRegion<FlashStorage>,
ota_data.as_embedded_storage(storage_ota)
);
let state_0 = ota_state(ota_slot::Slot0, ota_data);
let state_1 = ota_state(ota_slot::Slot1, ota_data);
let mut ota = Ota::new(ota_data)?;
let running = get_current_slot_and_fix_ota_data(&mut ota, state_0, state_1)?;
let target = running.next();
info!("Currently running OTA slot: {:?}", running);
info!("Slot0 state: {:?}", state_0);
info!("Slot1 state: {:?}", state_1);
//obtain current_state and next_state here!
let ota_target = match target {
Slot::None => {
panic!("No OTA slot active?");
}
Slot::Slot0 => pt
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App(
AppPartitionSubType::Ota0,
))?
.context("Partition table invalid no ota0")?,
Slot::Slot1 => pt
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App(
AppPartitionSubType::Ota1,
))?
.context("Partition table invalid no ota1")?,
};
let ota_target = mk_static!(PartitionEntry, ota_target);
let storage_ota = mk_static!(FlashStorage, FlashStorage::new());
let ota_target = mk_static!(
FlashRegion<FlashStorage>,
ota_target.as_embedded_storage(storage_ota)
);
let data_partition = pt
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data(
DataPartitionSubType::LittleFs,
))?
.expect("Data partition with littlefs not found");
let data_partition = mk_static!(PartitionEntry, data_partition);
let storage_data = mk_static!(FlashStorage, FlashStorage::new());
let data = mk_static!(
FlashRegion<FlashStorage>,
data_partition.as_embedded_storage(storage_data)
);
let lfs2filesystem = mk_static!(LittleFs2Filesystem, LittleFs2Filesystem { storage: data });
let alloc = mk_static!(Allocation<LittleFs2Filesystem>, lfs2Filesystem::allocate());
if lfs2filesystem.is_mountable() {
log::info!("Littlefs2 filesystem is mountable");
} else {
match lfs2filesystem.format() {
Result::Ok(..) => {
log::info!("Littlefs2 filesystem is formatted");
}
Err(err) => {
error!("Littlefs2 filesystem could not be formatted: {:?}", err);
}
}
}
let fs = Arc::new(Mutex::new(
lfs2Filesystem::mount(alloc, lfs2filesystem).expect("Could not mount lfs2 filesystem"),
));
let ap = interfaces.ap;
let sta = interfaces.sta;
let mut esp = Esp {
fs,
rng,
controller: Arc::new(Mutex::new(controller)),
interface_sta: Some(sta),
interface_ap: Some(ap),
boot_button,
wake_gpio1,
ota,
ota_target,
current: running,
slot0_state: state_0,
slot1_state: state_1,
};
//init,reset rtc memory depending on cause
let mut init_rtc_store: bool = false;
let mut to_config_mode: bool = false;
let reasons = match reset_reason() {
None => "unknown",
Some(reason) => match reason {
SocResetReason::ChipPowerOn => "power on",
SocResetReason::CoreSDIO => "sdio reset",
SocResetReason::CoreMwdt0 => "Watchdog Main",
SocResetReason::CoreMwdt1 => "Watchdog 1",
SocResetReason::CoreRtcWdt => "Watchdog RTC",
SocResetReason::Cpu0Mwdt0 => "Watchdog MCpu0",
SocResetReason::Cpu0Sw => "software reset cpu0",
SocResetReason::SysRtcWdt => "Watchdog Sys rtc",
SocResetReason::Cpu0Mwdt1 => "cpu0 mwdt1",
SocResetReason::SysSuperWdt => "Watchdog Super",
SocResetReason::Cpu0RtcWdt => {
init_rtc_store = true;
"Watchdog RTC cpu0"
}
SocResetReason::CoreSw => "software reset",
SocResetReason::CoreDeepSleep => "deep sleep",
SocResetReason::SysBrownOut => "sys brown out",
SocResetReason::CoreEfuseCrc => "core efuse crc",
SocResetReason::CoreUsbUart => {
//TODO still required? or via button ignore? to_config_mode = true;
to_config_mode = true;
"core usb uart"
}
SocResetReason::CoreUsbJtag => "core usb jtag",
SocResetReason::Cpu0JtagCpu => "cpu0 jtag cpu",
},
};
LOG_ACCESS
.lock()
.await
.log(
LogMessage::ResetReason,
init_rtc_store as u32,
to_config_mode as u32,
"",
&format!("{reasons:?}"),
)
.await;
esp.init_rtc_deepsleep_memory(init_rtc_store, to_config_mode)
.await;
let config = esp.load_config().await;
log::info!("Init rtc driver");
let sda = peripherals.GPIO20;
let scl = peripherals.GPIO19;
let i2c = I2c::new(
peripherals.I2C0,
Config::default()
.with_frequency(Rate::from_hz(100))
.with_timeout(BusTimeout::Maximum),
)?
.with_scl(scl)
.with_sda(sda);
let i2c_bus: embassy_sync::blocking_mutex::Mutex<
CriticalSectionRawMutex,
RefCell<I2c<Blocking>>,
> = CriticalSectionMutex::new(RefCell::new(i2c));
I2C_DRIVER.init(i2c_bus).expect("Could not init i2c driver");
let i2c_bus = I2C_DRIVER.get().await;
let rtc_device = I2cDevice::new(&i2c_bus);
let eeprom_device = I2cDevice::new(&i2c_bus);
let mut rtc: Ds323x<
I2cInterface<I2cDevice<CriticalSectionRawMutex, I2c<Blocking>>>,
DS3231,
> = Ds323x::new_ds3231(rtc_device);
info!("Init rtc eeprom driver");
let eeprom = Eeprom24x::new_24x32(eeprom_device, SlaveAddr::Alternative(true, true, true));
let rtc_time = rtc.datetime();
match rtc_time {
Ok(tt) => {
log::info!("Rtc Module reports time at UTC {}", tt);
}
Err(err) => {
log::info!("Rtc Module could not be read {:?}", err);
}
}
let storage: Storage<
I2cDevice<'static, CriticalSectionRawMutex, I2c<Blocking>>,
B32,
TwoBytes,
No,
Delay,
> = Storage::new(eeprom, Delay::new());
let rtc_module: Box<dyn RTCModuleInteraction + Send> =
Box::new(DS3231Module { rtc, storage }) as Box<dyn RTCModuleInteraction + Send>;
let hal = match config {
Result::Ok(config) => {
let battery_interaction: Box<dyn BatteryInteraction + Send> =
match config.hardware.battery {
BatteryBoardVersion::Disabled => Box::new(NoBatteryMonitor {}),
BatteryBoardVersion::BQ34Z100G1 => {
let battery_device = I2cDevice::new(I2C_DRIVER.get().await);
let mut battery_driver = Bq34z100g1Driver {
i2c: battery_device,
delay: Delay::new(),
flash_block_data: [0; 32],
};
let status = print_battery_bq34z100(&mut battery_driver);
match status {
Ok(_) => {}
Err(err) => {
LOG_ACCESS
.lock()
.await
.log(
LogMessage::BatteryCommunicationError,
0u32,
0,
"",
&format!("{err:?})"),
)
.await;
}
}
Box::new(BQ34Z100G1 { battery_driver })
}
BatteryBoardVersion::WchI2cSlave => {
// TODO use correct implementation once availible
Box::new(NoBatteryMonitor {})
}
};
let board_hal: Box<dyn BoardInteraction + Send> = match config.hardware.board {
BoardVersion::INITIAL => {
initial_hal::create_initial_board(free_pins, config, esp)?
}
BoardVersion::V3 => {
v3_hal::create_v3(free_pins, esp, config, battery_interaction, rtc_module)?
}
BoardVersion::V4 => {
v4_hal::create_v4(free_pins, esp, config, battery_interaction, rtc_module)
.await?
}
};
HAL { board_hal }
}
Err(err) => {
LOG_ACCESS
.lock()
.await
.log(
LogMessage::ConfigModeMissingConfig,
0,
0,
"",
&err.to_string(),
)
.await;
HAL {
board_hal: initial_hal::create_initial_board(
free_pins,
PlantControllerConfig::default(),
esp,
)?,
}
}
};
Ok(Mutex::new(hal))
}
}
fn ota_state(slot: ota_slot, ota_data: &mut FlashRegion<FlashStorage>) -> OtaImageState {
// Read and log OTA states for both slots before constructing Ota
// Each OTA select entry is 32 bytes: [seq:4][label:20][state:4][crc:4]
// Offsets within the OTA data partition: slot0 @ 0x0000, slot1 @ 0x1000
if slot == ota_slot::None {
return OtaImageState::Undefined;
}
let mut slot_buf = [0u8; 32];
if slot == ota_slot::Slot0 {
let _ = ota_data.read(0x0000, &mut slot_buf);
} else {
let _ = ota_data.read(0x1000, &mut slot_buf);
}
let raw_state = u32::from_le_bytes(slot_buf[24..28].try_into().unwrap_or([0xff; 4]));
let state0 = OtaImageState::try_from(raw_state).unwrap_or(OtaImageState::Undefined);
state0
}
fn get_current_slot_and_fix_ota_data(
ota: &mut Ota<FlashStorage>,
state0: OtaImageState,
state1: OtaImageState,
) -> Result<ota_slot, FatError> {
let state = ota.current_ota_state().unwrap_or_default();
let swap = match state {
OtaImageState::Invalid => true,
OtaImageState::Aborted => true,
OtaImageState::Undefined => {
info!("Undefined image in current slot, bootloader wrong?");
false
}
_ => false,
};
let current = ota.current_slot()?;
if swap {
let other = match current {
ota_slot::Slot0 => state1,
ota_slot::Slot1 => state0,
_ => OtaImageState::Invalid,
};
match other {
OtaImageState::Invalid => {
bail!(
"cannot recover slot, as both slots in invalid state {:?} {:?} {:?}",
current,
state0,
state1
);
}
OtaImageState::Aborted => {
bail!(
"cannot recover slot, as both slots in invalid state {:?} {:?} {:?}",
current,
state0,
state1
);
}
_ => {}
}
info!(
"Current slot has state {:?} other state has {:?} swapping",
state, other
);
ota.set_current_slot(current.next())?;
//we actually booted other slot, than partition table assumes
return Ok(ota.current_slot()?);
};
Ok(current)
}
pub async fn esp_time() -> DateTime<Utc> {
let guard = TIME_ACCESS.get().await.lock().await;
DateTime::from_timestamp_micros(guard.current_time_us() as i64).unwrap()
}
pub async fn esp_set_time(time: DateTime<FixedOffset>) -> FatResult<()> {
{
let guard = TIME_ACCESS.get().await.lock().await;
guard.set_current_time_us(time.timestamp_micros() as u64);
}
BOARD_ACCESS
.get()
.await
.lock()
.await
.board_hal
.get_rtc_module()
.set_rtc_time(&time.to_utc())
.await
}