465 lines
16 KiB
Rust
465 lines
16 KiB
Rust
use crate::bail;
|
|
use crate::config::{NetworkConfig, PlantControllerConfig};
|
|
use crate::hal::PLANT_COUNT;
|
|
use crate::log::{log, LogMessage};
|
|
use alloc::vec;
|
|
use chrono::{DateTime, Utc};
|
|
use esp_hal::Blocking;
|
|
use esp_hal::uart::Uart;
|
|
use serde::Serialize;
|
|
|
|
use crate::fat_error::{ContextExt, FatError, FatResult};
|
|
use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem;
|
|
use alloc::string::ToString;
|
|
use alloc::sync::Arc;
|
|
use alloc::{format, string::String, vec::Vec};
|
|
use core::net::{IpAddr, Ipv4Addr, SocketAddr};
|
|
use core::str::FromStr;
|
|
use core::sync::atomic::Ordering;
|
|
use embassy_executor::Spawner;
|
|
use embassy_net::{DhcpConfig, IpAddress, Ipv4Cidr, Runner, Stack, StackResources, StaticConfigV4};
|
|
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
|
use embassy_sync::mutex::{Mutex, MutexGuard};
|
|
use embassy_time::{Duration, Timer, WithTimeout};
|
|
use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash, RmwNorFlashStorage};
|
|
use esp_bootloader_esp_idf::ota::OtaImageState::Valid;
|
|
use esp_bootloader_esp_idf::ota::{Ota, OtaImageState};
|
|
use esp_bootloader_esp_idf::partitions::{AppPartitionSubType, FlashRegion};
|
|
use esp_hal::gpio::{Input, RtcPinWithResistors};
|
|
use esp_hal::rng::Rng;
|
|
use esp_hal::rtc_cntl::{
|
|
sleep::{TimerWakeupSource, WakeupLevel},
|
|
Rtc,
|
|
};
|
|
use esp_hal::system::software_reset;
|
|
use esp_println::println;
|
|
use esp_radio::wifi::ap::{AccessPointConfig, AccessPointInfo};
|
|
use esp_radio::wifi::scan::{ScanConfig, ScanTypeConfig};
|
|
use esp_radio::wifi::sta::StationConfig;
|
|
use esp_radio::wifi::{AuthenticationMethod, Config, Interface, WifiController};
|
|
use esp_storage::FlashStorage;
|
|
use littlefs2::fs::Filesystem;
|
|
use littlefs2_core::{FileType, PathBuf, SeekFrom};
|
|
use log::{info, warn, error};
|
|
use portable_atomic::AtomicBool;
|
|
|
|
use super::shared_flash::MutexFlashStorage;
|
|
use crate::network::{net_task, run_dhcp};
|
|
|
|
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
|
|
static mut LAST_WATERING_TIMESTAMP: [i64; PLANT_COUNT] = [0; PLANT_COUNT];
|
|
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
|
|
static mut CONSECUTIVE_WATERING_PLANT: [u32; PLANT_COUNT] = [0; PLANT_COUNT];
|
|
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
|
|
static mut LOW_VOLTAGE_DETECTED: i8 = 0;
|
|
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
|
|
static mut RESTART_TO_CONF: i8 = 0;
|
|
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
|
|
static mut LAST_CORROSION_PROTECTION_CHECK_DAY: i8 = -1;
|
|
|
|
|
|
const CONFIG_FILE: &str = "config.json";
|
|
|
|
#[derive(Serialize, Debug)]
|
|
pub struct FileInfo {
|
|
filename: String,
|
|
size: usize,
|
|
}
|
|
|
|
#[derive(Serialize, Debug)]
|
|
pub struct FileList {
|
|
total: usize,
|
|
used: usize,
|
|
files: Vec<FileInfo>,
|
|
}
|
|
|
|
// Minimal esp-idf equivalent for gpio_hold on esp32c6 via ROM functions
|
|
extern "C" {
|
|
fn gpio_pad_hold(gpio_num: u32);
|
|
fn gpio_pad_unhold(gpio_num: u32);
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn hold_enable(gpio_num: u8) {
|
|
unsafe { gpio_pad_hold(gpio_num as u32) }
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn hold_disable(gpio_num: u8) {
|
|
unsafe { gpio_pad_unhold(gpio_num as u32) }
|
|
}
|
|
|
|
pub struct Esp<'a> {
|
|
pub fs: Arc<Mutex<CriticalSectionRawMutex, Filesystem<'static, LittleFs2Filesystem>>>,
|
|
pub rng: Rng,
|
|
//first starter (ap or sta will take these)
|
|
pub interface_sta: Option<Interface<'static>>,
|
|
pub interface_ap: Option<Interface<'static>>,
|
|
pub controller: Arc<Mutex<CriticalSectionRawMutex, WifiController<'static>>>,
|
|
|
|
pub boot_button: Input<'a>,
|
|
|
|
// RTC-capable GPIO used as external wake source (store the raw peripheral)
|
|
pub wake_gpio1: esp_hal::peripherals::GPIO1<'static>,
|
|
pub uart0: Uart<'a, Blocking>,
|
|
|
|
pub rtc: Rtc<'a>,
|
|
|
|
pub ota: Ota<'static, RmwNorFlashStorage<'static, &'static mut MutexFlashStorage>>,
|
|
pub ota_target: &'static mut FlashRegion<'static, MutexFlashStorage>,
|
|
pub current: AppPartitionSubType,
|
|
pub slot0_state: OtaImageState,
|
|
pub slot1_state: OtaImageState,
|
|
}
|
|
|
|
// SAFETY: On this target we never move Esp across OS threads; the firmware runs single-core
|
|
// cooperative tasks with Embassy. All interior mutability of non-Send peripherals is gated
|
|
// behind &mut self or embassy_sync Mutex with CriticalSectionRawMutex, which does not rely on
|
|
// thread scheduling. Therefore it is sound to mark Esp as Send to satisfy trait object bounds
|
|
// (e.g., Box<dyn BoardInteraction + Send>). If you add fields that are accessed from multiple
|
|
// CPU cores/threads, reconsider this.
|
|
unsafe impl Send for Esp<'_> {}
|
|
|
|
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 Esp<'_> {
|
|
pub fn get_time(&self) -> DateTime<Utc> {
|
|
DateTime::from_timestamp_micros(self.rtc.current_time_us() as i64)
|
|
.unwrap_or(DateTime::UNIX_EPOCH)
|
|
}
|
|
|
|
pub fn set_time(&mut self, time: DateTime<Utc>) {
|
|
self.rtc.set_current_time_us(time.timestamp_micros() as u64);
|
|
}
|
|
|
|
pub(crate) async fn read_serial_line(&mut self) -> FatResult<Option<alloc::string::String>> {
|
|
let mut buf = [0u8; 1];
|
|
let mut line = String::new();
|
|
loop {
|
|
match self.uart0.read_buffered(&mut buf) {
|
|
Ok(read) => {
|
|
if read == 0 {
|
|
return Ok(None);
|
|
}
|
|
let c = buf[0] as char;
|
|
if c == '\n' {
|
|
return Ok(Some(line));
|
|
}
|
|
line.push(c);
|
|
}
|
|
Err(error) => {
|
|
if line.is_empty() {
|
|
return Ok(None);
|
|
} else {
|
|
error!("Error reading serial line: {error:?}");
|
|
// If we already have some data, we should probably wait a bit or just return what we have?
|
|
// But the protocol expects a full line or message.
|
|
// For simplicity in config mode, we can block here or just return None if nothing is there yet.
|
|
// However, if we started receiving, we should probably finish or timeout.
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
pub(crate) async fn delete_file(&self, filename: String) -> FatResult<()> {
|
|
let file = PathBuf::try_from(filename.as_str())?;
|
|
let access = self.fs.lock().await;
|
|
access.remove(&*file)?;
|
|
Ok(())
|
|
}
|
|
pub(crate) async fn write_file(
|
|
&mut self,
|
|
filename: String,
|
|
offset: u32,
|
|
buf: &[u8],
|
|
) -> Result<(), FatError> {
|
|
let file = PathBuf::try_from(filename.as_str())?;
|
|
let access = self.fs.lock().await;
|
|
access.open_file_with_options_and_then(
|
|
|options| options.read(true).write(true).create(true),
|
|
&*file,
|
|
|file| {
|
|
file.seek(SeekFrom::Start(offset))?;
|
|
file.write(buf)?;
|
|
Ok(())
|
|
},
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn get_size(&mut self, filename: String) -> FatResult<usize> {
|
|
let file = PathBuf::try_from(filename.as_str())?;
|
|
let access = self.fs.lock().await;
|
|
let data = access.metadata(&*file)?;
|
|
Ok(data.len())
|
|
}
|
|
pub(crate) async fn get_file(
|
|
&mut self,
|
|
filename: String,
|
|
chunk: u32,
|
|
) -> FatResult<([u8; 512], usize)> {
|
|
use littlefs2::io::Error as lfs2Error;
|
|
|
|
let file = PathBuf::try_from(filename.as_str())?;
|
|
let access = self.fs.lock().await;
|
|
let mut buf = [0_u8; 512];
|
|
let mut read = 0;
|
|
let offset = chunk * buf.len() as u32;
|
|
access.open_file_with_options_and_then(
|
|
|options| options.read(true),
|
|
&*file,
|
|
|file| {
|
|
let length = file.len()? as u32;
|
|
if length == 0 {
|
|
Err(lfs2Error::IO)
|
|
} else if length > offset {
|
|
file.seek(SeekFrom::Start(offset))?;
|
|
read = file.read(&mut buf)?;
|
|
Ok(())
|
|
} else {
|
|
//exactly at end, do nothing
|
|
Ok(())
|
|
}
|
|
},
|
|
)?;
|
|
Ok((buf, read))
|
|
}
|
|
|
|
pub(crate) async fn write_ota(&mut self, offset: u32, buf: &[u8]) -> Result<(), FatError> {
|
|
let _ = check_erase(self.ota_target, offset, offset + 4096);
|
|
info!("erasing and writing block 0x{offset:x}");
|
|
self.ota_target.erase(offset, offset + 4096)?;
|
|
|
|
let mut temp = vec![0; buf.len()];
|
|
let read_back = temp.as_mut_slice();
|
|
//change to nor flash, align writes!
|
|
self.ota_target.write(offset, buf)?;
|
|
self.ota_target.read(offset, read_back)?;
|
|
if buf != read_back {
|
|
info!("Expected {buf:?} but got {read_back:?}");
|
|
bail!(
|
|
"Flash error, read back does not match write buffer at offset {:x}",
|
|
offset
|
|
)
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) async fn finalize_ota(&mut self) -> Result<(), FatError> {
|
|
let current = self.ota.current_app_partition()?;
|
|
if self.ota.current_ota_state()? != Valid {
|
|
info!("Validating current slot {current:?} as it was able to ota");
|
|
self.ota.set_current_ota_state(Valid)?;
|
|
}
|
|
let next = match current {
|
|
AppPartitionSubType::Ota0 => AppPartitionSubType::Ota1,
|
|
AppPartitionSubType::Ota1 => AppPartitionSubType::Ota0,
|
|
_ => {
|
|
bail!("Invalid current slot {current:?} for ota");
|
|
}
|
|
};
|
|
self.ota.set_current_app_partition(next)?;
|
|
info!("switched slot");
|
|
self.ota.set_current_ota_state(OtaImageState::New)?;
|
|
info!("switched state for new partition");
|
|
let state_new = self.ota.current_ota_state()?;
|
|
info!("state on new partition now {state_new:?}");
|
|
self.set_restart_to_conf(true);
|
|
Ok(())
|
|
}
|
|
|
|
// let current = ota.current_slot()?;
|
|
// println!(
|
|
// "current image state {:?} (only relevant if the bootloader was built with auto-rollback support)",
|
|
// ota.current_ota_state()
|
|
// );
|
|
// println!("current {:?} - next {:?}", current, current.next());
|
|
// let ota_state = ota.current_ota_state()?;
|
|
|
|
pub(crate) fn mode_override_pressed(&mut self) -> bool {
|
|
self.boot_button.is_low()
|
|
}
|
|
|
|
pub(crate) async fn wifi_scan(&mut self) -> FatResult<Vec<AccessPointInfo>> {
|
|
info!("start wifi scan");
|
|
let mut lock = self.controller.try_lock()?;
|
|
info!("start wifi scan lock");
|
|
let scan_config = ScanConfig::default().with_scan_type(ScanTypeConfig::Active {
|
|
min: esp_hal::time::Duration::from_millis(0),
|
|
max: esp_hal::time::Duration::from_millis(0),
|
|
});
|
|
let rv = lock.scan_async(&scan_config).await?;
|
|
info!("end wifi scan lock");
|
|
Ok(rv)
|
|
}
|
|
|
|
pub(crate) fn last_pump_time(&self, plant: usize) -> Option<DateTime<Utc>> {
|
|
let ts = unsafe { LAST_WATERING_TIMESTAMP }[plant];
|
|
DateTime::from_timestamp_millis(ts)
|
|
}
|
|
pub(crate) fn store_last_pump_time(&mut self, plant: usize, time: DateTime<Utc>) {
|
|
unsafe {
|
|
LAST_WATERING_TIMESTAMP[plant] = time.timestamp_millis();
|
|
}
|
|
}
|
|
pub(crate) fn set_low_voltage_in_cycle(&mut self) {
|
|
unsafe {
|
|
LOW_VOLTAGE_DETECTED = 1;
|
|
}
|
|
}
|
|
pub(crate) fn clear_low_voltage_in_cycle(&mut self) {
|
|
unsafe {
|
|
LOW_VOLTAGE_DETECTED = 0;
|
|
}
|
|
}
|
|
|
|
pub(crate) fn low_voltage_in_cycle(&mut self) -> bool {
|
|
unsafe { LOW_VOLTAGE_DETECTED == 1 }
|
|
}
|
|
pub(crate) fn store_consecutive_pump_count(&mut self, plant: usize, count: u32) {
|
|
unsafe {
|
|
CONSECUTIVE_WATERING_PLANT[plant] = count;
|
|
}
|
|
}
|
|
pub(crate) fn consecutive_pump_count(&mut self, plant: usize) -> u32 {
|
|
unsafe { CONSECUTIVE_WATERING_PLANT[plant] }
|
|
}
|
|
pub(crate) fn get_restart_to_conf(&mut self) -> bool {
|
|
unsafe { RESTART_TO_CONF == 1 }
|
|
}
|
|
pub(crate) fn set_restart_to_conf(&mut self, to_conf: bool) {
|
|
unsafe {
|
|
if to_conf {
|
|
RESTART_TO_CONF = 1;
|
|
} else {
|
|
RESTART_TO_CONF = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn deep_sleep_ms(&mut self, duration_in_ms: u64) -> ! {
|
|
// Mark the current OTA image as valid if we reached here while in pending verify.
|
|
if let Ok(cur) = self.ota.current_ota_state() {
|
|
if cur == OtaImageState::PendingVerify {
|
|
info!("Marking OTA image as valid");
|
|
if let Err(err) = self.ota.set_current_ota_state(Valid) {
|
|
error!("Could not set image to valid: {:?}", err);
|
|
}
|
|
}
|
|
} else {
|
|
info!("No OTA image to mark as valid");
|
|
}
|
|
|
|
if duration_in_ms == 0 {
|
|
software_reset();
|
|
} else {
|
|
let timer = TimerWakeupSource::new(core::time::Duration::from_millis(duration_in_ms));
|
|
let mut wake_pins: [(&mut dyn RtcPinWithResistors, WakeupLevel); 1] =
|
|
[(&mut self.wake_gpio1, WakeupLevel::Low)];
|
|
let ext1 = esp_hal::rtc_cntl::sleep::Ext1WakeupSource::new(&mut wake_pins);
|
|
self.rtc.sleep_deep(&[&timer, &ext1]);
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn load_config(&mut self) -> FatResult<PlantControllerConfig> {
|
|
let cfg = PathBuf::try_from(CONFIG_FILE).unwrap();
|
|
let data = self.fs.lock().await.read::<4096>(&cfg)?;
|
|
let config: PlantControllerConfig = serde_json::from_slice(&data)?;
|
|
return Ok(config);
|
|
}
|
|
pub(crate) async fn save_config(&mut self, config: Vec<u8>) -> FatResult<()> {
|
|
let filesystem = self.fs.lock().await;
|
|
let cfg = PathBuf::try_from(CONFIG_FILE)?;
|
|
filesystem.write(&cfg, &*config)?;
|
|
Ok(())
|
|
}
|
|
pub(crate) async fn list_files(&self) -> FatResult<FileList> {
|
|
let path = PathBuf::new();
|
|
|
|
let fs = self.fs.lock().await;
|
|
let free_size = fs.available_space()?;
|
|
let total_size = fs.total_space();
|
|
|
|
let mut result = FileList {
|
|
total: total_size,
|
|
used: total_size - free_size,
|
|
files: Vec::new(),
|
|
};
|
|
|
|
fs.read_dir_and_then(&path, |dir| {
|
|
for entry in dir {
|
|
let e = entry?;
|
|
if e.file_type() == FileType::File {
|
|
result.files.push(FileInfo {
|
|
filename: e.path().to_string(),
|
|
size: e.metadata().len(),
|
|
});
|
|
}
|
|
}
|
|
Ok(())
|
|
})?;
|
|
Ok(result)
|
|
}
|
|
|
|
pub(crate) async fn init_rtc_deepsleep_memory(
|
|
&self,
|
|
init_rtc_store: bool,
|
|
to_config_mode: bool,
|
|
) {
|
|
if init_rtc_store {
|
|
unsafe {
|
|
LAST_WATERING_TIMESTAMP = [0; PLANT_COUNT];
|
|
CONSECUTIVE_WATERING_PLANT = [0; PLANT_COUNT];
|
|
LOW_VOLTAGE_DETECTED = 0;
|
|
if to_config_mode {
|
|
RESTART_TO_CONF = 1
|
|
} else {
|
|
RESTART_TO_CONF = 0;
|
|
}
|
|
LAST_CORROSION_PROTECTION_CHECK_DAY = -1;
|
|
};
|
|
} else {
|
|
unsafe {
|
|
if to_config_mode {
|
|
RESTART_TO_CONF = 1;
|
|
}
|
|
log(
|
|
LogMessage::RestartToConfig,
|
|
RESTART_TO_CONF as u32,
|
|
0,
|
|
"",
|
|
"",
|
|
);
|
|
log(
|
|
LogMessage::LowVoltage,
|
|
LOW_VOLTAGE_DETECTED as u32,
|
|
0,
|
|
"",
|
|
"",
|
|
);
|
|
// is executed before main, no other code will alter these values during printing
|
|
#[allow(static_mut_refs)]
|
|
for (i, time) in LAST_WATERING_TIMESTAMP.iter().enumerate() {
|
|
info!("LAST_WATERING_TIMESTAMP[{i}] = UTC {time}");
|
|
}
|
|
// is executed before main, no other code will alter these values during printing
|
|
#[allow(static_mut_refs)]
|
|
for (i, item) in CONSECUTIVE_WATERING_PLANT.iter().enumerate() {
|
|
info!("CONSECUTIVE_WATERING_PLANT[{i}] = {item}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|