diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 8a6cd59..8afedbf 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -159,7 +159,7 @@ edge-nal-embassy = "0.6.0" static_cell = "2.1.1" cfg-if = "1.0.3" edge-http = { version = "0.6.1", features = ["log"] } -littlefs2 = { version = "0.6.1", features = ["c-stubs"] } +littlefs2 = { version = "0.6.1", features = ["c-stubs", "alloc", "serde"] } littlefs2-core = "0.1.1" diff --git a/rust/espflash.toml b/rust/espflash.toml index f2a3eb8..207afc6 100644 --- a/rust/espflash.toml +++ b/rust/espflash.toml @@ -1,6 +1,8 @@ -format = "EspIdf" +[connection] -[idf_format_args] +[[usb_device]] +vid = "303a" +pid = "1001" [flash] size = "16MB" diff --git a/rust/partitions.csv b/rust/partitions.csv index dfa957b..7372605 100644 --- a/rust/partitions.csv +++ b/rust/partitions.csv @@ -3,4 +3,4 @@ otadata, data, ota, , 8k, phy_init, data, phy, , 4k, ota_0, app, ota_0, , 3968k, ota_1, app, ota_1, , 3968k, -storage, data, spiffs, , 8M, +storage, data, littlefs,, 8M, diff --git a/rust/src/hal/LittleFS2StorageAdapter.rs b/rust/src/hal/LittleFS2StorageAdapter.rs index fb03951..92a31f6 100644 --- a/rust/src/hal/LittleFS2StorageAdapter.rs +++ b/rust/src/hal/LittleFS2StorageAdapter.rs @@ -1,4 +1,4 @@ -use embedded_storage::nor_flash::{NorFlash, ReadNorFlash}; +use embedded_storage::{ReadStorage, Storage}; use esp_bootloader_esp_idf::partitions::FlashRegion; use esp_storage::FlashStorage; use littlefs2::consts::U1 as lfs2Array1; @@ -7,7 +7,7 @@ use littlefs2::driver::Storage as lfs2Storage; use littlefs2::fs::Filesystem as lfs2Filesystem; use littlefs2::io::Error as lfs2Error; use littlefs2::io::Result as lfs2Result; -use log::error; +use log::{error, info}; pub struct LittleFs2Filesystem { pub(crate) storage: &'static mut FlashRegion<'static, FlashStorage>, @@ -16,13 +16,18 @@ pub struct LittleFs2Filesystem { impl lfs2Storage for LittleFs2Filesystem { const READ_SIZE: usize = 512; const WRITE_SIZE: usize = 512; - const BLOCK_SIZE: usize = 32 * 1024; //usually optimal for flash access - const BLOCK_COUNT: usize = 8 * 1024 * 1024 / 32 * 1024; //8mb in 32blocks - const BLOCK_CYCLES: isize = 0; + const BLOCK_SIZE: usize = 1024; //usually optimal for flash access + const BLOCK_COUNT: usize = 8 * 1024 * 1024 / 1024; //8mb in 32kb blocks + const BLOCK_CYCLES: isize = 100; type CACHE_SIZE = lfs2Array512; type LOOKAHEAD_SIZE = lfs2Array1; fn read(&mut self, off: usize, buf: &mut [u8]) -> lfs2Result { + info!( + "Littlefs2Filesystem read at offset {} with len {}", + off, + buf.len() + ); let read_size: usize = Self::READ_SIZE; assert_eq!(off % read_size, 0); assert_eq!(buf.len() % read_size, 0); @@ -36,6 +41,11 @@ impl lfs2Storage for LittleFs2Filesystem { } fn write(&mut self, off: usize, data: &[u8]) -> lfs2Result { + info!( + "Littlefs2Filesystem write at offset {} with len {}", + off, + data.len() + ); let write_size: usize = Self::WRITE_SIZE; assert_eq!(off % write_size, 0); assert_eq!(data.len() % write_size, 0); @@ -49,15 +59,20 @@ impl lfs2Storage for LittleFs2Filesystem { } fn erase(&mut self, off: usize, len: usize) -> lfs2Result { + info!( + "Littlefs2Filesystem erase at offset {} with len {}", + off, len + ); let block_size: usize = Self::BLOCK_SIZE; debug_assert!(off % block_size == 0); debug_assert!(len % block_size == 0); - match self.storage.erase(off as u32, len as u32) { - anyhow::Result::Ok(..) => lfs2Result::Ok(len), - Err(err) => { - error!("Littlefs2Filesystem erase error: {:?}", err); - Err(lfs2Error::IO) - } - } + //match self.storage.erase(off as u32, len as u32) { + //anyhow::Result::Ok(..) => lfs2Result::Ok(len), + //Err(err) => { + //error!("Littlefs2Filesystem erase error: {:?}", err); + //Err(lfs2Error::IO) + // } + //} + lfs2Result::Ok(len) } } diff --git a/rust/src/hal/esp.rs b/rust/src/hal/esp.rs index 3279cc4..a375d68 100644 --- a/rust/src/hal/esp.rs +++ b/rust/src/hal/esp.rs @@ -46,6 +46,8 @@ static mut LOW_VOLTAGE_DETECTED: bool = false; #[link_section = ".rtc.data"] static mut RESTART_TO_CONF: bool = false; +static CONFIG_FILE: &str = "config.json"; + #[derive(Serialize, Debug)] pub struct FileInfo { filename: String, @@ -87,6 +89,14 @@ pub struct Esp<'a> { pub ota_next: &'static mut FlashRegion<'static, FlashStorage>, } +// 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). If you add fields that are accessed from multiple +// CPU cores/threads, reconsider this. +unsafe impl Send for Esp<'_> {} + pub struct IpInfo { pub(crate) ip: IpAddr, netmask: IpAddr, @@ -224,7 +234,7 @@ impl Esp<'_> { } pub(crate) async fn wifi_ap(&mut self) -> anyhow::Result> { - let ssid = match self.load_config() { + let ssid = match self.load_config().await { Ok(config) => config.network.ap_ssid.as_str().to_string(), Err(_) => "PlantCtrl Emergency Mode".to_string(), }; @@ -370,50 +380,29 @@ impl Esp<'_> { // log(LogMessage::WifiInfo, 0, 0, "", &format!("{address:?}")); // anyhow::Ok(address) } - pub(crate) fn load_config(&mut self) -> anyhow::Result { - bail!("todo"); - // let cfg = File::open(Self::CONFIG_FILE)?; - // let config: PlantControllerConfig = serde_json::from_reader(cfg)?; - // anyhow::Ok(config) + pub(crate) async fn load_config(&mut self) -> anyhow::Result { + let cfg = PathBuf::try_from(CONFIG_FILE).unwrap(); + match self.fs.lock().await.read::<4096>(&cfg) { + Ok(data) => { + let config: PlantControllerConfig = serde_json::from_slice(&data)?; + anyhow::Ok(config) + } + Err(err) => { + bail!(format!("{err:?}")) + } + } } - pub(crate) async fn save_config( - &mut self, - _config: &PlantControllerConfig, - ) -> anyhow::Result<()> { - bail!("todo"); - // let mut cfg = File::create(Self::CONFIG_FILE)?; - // serde_json::to_writer(&mut cfg, &config)?; - // log::info!("Wrote config config {:?}", config); - // anyhow::Ok(()) - } - pub(crate) fn mount_file_system(&mut self) -> anyhow::Result<()> { - bail!("fail"); - // log(LogMessage::MountingFilesystem, 0, 0, "", ""); - // let base_path = String::try_from("/spiffs")?; - // let storage = String::try_from(Self::SPIFFS_PARTITION_NAME)?; - //let conf = todo!(); + pub(crate) async fn save_config(&mut self, config: Vec) -> anyhow::Result<()> { + let filesystem = self.fs.lock().await; + let cfg = PathBuf::try_from(CONFIG_FILE).unwrap(); - //let conf = esp_idf_sys::esp_vfs_spiffs_conf_t { - //base_path: base_path.as_ptr(), - //partition_label: storage.as_ptr(), - //max_files: 5, - //format_if_mount_failed: true, - //}; - - //TODO - //unsafe { - //esp_idf_sys::esp!(esp_idf_sys::esp_vfs_spiffs_register(&conf))?; - //} - - // let free_space = self.file_system_size()?; - // log( - // LogMessage::FilesystemMount, - // free_space.free_size as u32, - // free_space.total_size as u32, - // &free_space.used_size.to_string(), - // "", - // ); - // anyhow::Ok(()) + match filesystem.write(&cfg, &*config) { + Ok(_) => {} + Err(err) => { + bail!(format!("{err:?}")) + } + } + anyhow::Ok(()) } async fn file_system_size(&mut self) -> anyhow::Result { bail!("fail"); diff --git a/rust/src/hal/initial_hal.rs b/rust/src/hal/initial_hal.rs index 5df79a9..54155ee 100644 --- a/rust/src/hal/initial_hal.rs +++ b/rust/src/hal/initial_hal.rs @@ -125,11 +125,8 @@ impl<'a> BoardInteraction<'a> for Initial<'a> { bail!("Please configure board revision") } - async fn set_config(&mut self, config: PlantControllerConfig) -> anyhow::Result<()> { + fn set_config(&mut self, config: PlantControllerConfig) { self.config = config; - //TODO - // self.esp.save_config(&self.config)?; - anyhow::Ok(()) } async fn get_mptt_voltage(&mut self) -> Result { diff --git a/rust/src/hal/mod.rs b/rust/src/hal/mod.rs index 091b62c..0dc7d31 100644 --- a/rust/src/hal/mod.rs +++ b/rust/src/hal/mod.rs @@ -49,6 +49,7 @@ use esp_storage::FlashStorage; use esp_wifi::{init, EspWifiController}; use littlefs2::fs::{Allocation, Filesystem as lfs2Filesystem}; use littlefs2::object_safe::DynStorage; +use log::{info, warn}; //Only support for 8 right now! pub const PLANT_COUNT: usize = 8; @@ -88,7 +89,7 @@ pub trait BoardInteraction<'a> { async fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result; async fn general_fault(&mut self, enable: bool); async fn test(&mut self) -> Result<()>; - async fn set_config(&mut self, config: PlantControllerConfig) -> Result<()>; + fn set_config(&mut self, config: PlantControllerConfig); async fn get_mptt_voltage(&mut self) -> anyhow::Result; async fn get_mptt_current(&mut self) -> anyhow::Result; } @@ -99,7 +100,12 @@ impl dyn BoardInteraction<'_> { let even = counter % 2 == 0; let current = counter / (PLANT_COUNT as u32); for led in 0..PLANT_COUNT { - self.fault(led, current == led as u32).await.unwrap(); + match self.fault(led, current == led as u32).await { + Result::Ok(_) => {} + Err(err) => { + warn!("Fault on plant {}: {:?}", led, err); + } + } } let _ = self.general_fault(even.into()); } @@ -187,11 +193,11 @@ impl PlantHal { let timg0 = TimerGroup::new(peripherals.TIMG0); let esp_wifi_ctrl = &*mk_static!( EspWifiController<'static>, - init(timg0.timer0, rng.clone()).unwrap() + init(timg0.timer0, rng.clone()).expect("Could not init wifi controller") ); let (controller, interfaces) = - esp_wifi::wifi::new(&esp_wifi_ctrl, peripherals.WIFI).unwrap(); + 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); @@ -234,11 +240,9 @@ impl PlantHal { }; // - let tablebuffer: [u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN] = - [0u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN]; let tablebuffer = mk_static!( [u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN], - tablebuffer + [0u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN] ); let storage_ota = mk_static!(FlashStorage, FlashStorage::new()); let pt = @@ -246,36 +250,37 @@ impl PlantHal { // List all partitions - this is just FYI for i in 0..pt.len() { - println!("{:?}", pt.get_partition(i)); + info!("{:?}", pt.get_partition(i)); } - - let ota_data = pt - .find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data( + let ota_data = mk_static!( + PartitionEntry, + pt.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data( DataPartitionSubType::Ota, ))? - .unwrap(); + .expect("No OTA data partition found") + ); - let ota_data = mk_static!(PartitionEntry, ota_data); - - let ota_data = ota_data.as_embedded_storage(storage_ota); - let ota_data = mk_static!(FlashRegion, ota_data); + let ota_data = mk_static!( + FlashRegion, + ota_data.as_embedded_storage(storage_ota) + ); let mut ota = esp_bootloader_esp_idf::ota::Ota::new(ota_data)?; let ota_partition = match ota.current_slot()? { Slot::None => { - panic!("No OTA slot found"); + panic!("No OTA slot active?"); } Slot::Slot0 => pt .find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App( AppPartitionSubType::Ota0, ))? - .unwrap(), + .expect("No OTA slot0 found"), Slot::Slot1 => pt .find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App( AppPartitionSubType::Ota1, ))? - .unwrap(), + .expect("No OTA slot1 found"), }; let ota_next = mk_static!(PartitionEntry, ota_partition); @@ -289,11 +294,11 @@ impl PlantHal { .find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data( DataPartitionSubType::LittleFs, ))? - .unwrap(); + .expect("Data partition with littlefs not found"); let data_partition = mk_static!(PartitionEntry, data_partition); let storage_data = mk_static!(FlashStorage, FlashStorage::new()); - let mut data = mk_static!( + let data = mk_static!( FlashRegion, data_partition.as_embedded_storage(storage_data) ); @@ -313,7 +318,7 @@ impl PlantHal { } let fs = Arc::new(Mutex::new( - lfs2Filesystem::mount(alloc, lfs2filesystem).unwrap(), + lfs2Filesystem::mount(alloc, lfs2filesystem).expect("Could not mount lfs2 filesystem"), )); let mut esp = Esp { @@ -362,9 +367,8 @@ impl PlantHal { ); esp.init_rtc_deepsleep_memory(init_rtc_store, to_config_mode); - let fs_mount_error = esp.mount_file_system().is_err(); - let config = esp.load_config(); + let config = esp.load_config().await; log::info!("Init rtc driver"); // let mut rtc = Ds323x::new_ds3231(MutexDevice::new(&I2C_DRIVER)); diff --git a/rust/src/main.rs b/rust/src/main.rs index 7423a4f..a4074bc 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -232,8 +232,10 @@ async fn safe_main(spawner: Spawner) -> anyhow::Result<()> { info!("no mode override"); } - if board.board_hal.get_config().hardware.board == INITIAL - && board.board_hal.get_config().network.ssid.is_none() + //TODO hack + if true + || (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"); diff --git a/rust/src/webserver/mod.rs b/rust/src/webserver/mod.rs index 355c083..7f568b3 100644 --- a/rust/src/webserver/mod.rs +++ b/rust/src/webserver/mod.rs @@ -1,17 +1,21 @@ //offer ota and config mode +use crate::config::PlantControllerConfig; use crate::{get_version, log::LogMessage, BOARD_ACCESS}; +use alloc::borrow::ToOwned; use alloc::string::{String, ToString}; use alloc::sync::Arc; use alloc::vec::Vec; +use anyhow::bail; use core::fmt::{Debug, Display}; use core::net::{IpAddr, Ipv4Addr, SocketAddr}; use core::result::Result::Ok; +use core::str::from_utf8; use core::sync::atomic::{AtomicBool, Ordering}; use edge_http::io::server::{Connection, Handler, Server}; use edge_http::io::Error; use edge_http::Method; -use edge_nal::TcpBind; +use edge_nal::{TcpBind, TcpSplit}; use edge_nal_embassy::{Tcp, TcpBuffers}; use embassy_net::Stack; use embassy_time::Instant; @@ -185,16 +189,7 @@ pub struct NightLampCommand { // anyhow::Ok(Some(json)) // } // -// fn set_config( -// request: &mut Request<&mut EspHttpConnection>, -// ) -> Result, anyhow::Error> { -// let all = read_up_to_bytes_from_request(request, Some(4096))?; -// let config: PlantControllerConfig = serde_json::from_slice(&all)?; -// -// let mut board = BOARD_ACCESS.lock().expect("board access"); -// board.board_hal.set_config(config)?; -// anyhow::Ok(Some("saved".to_owned())) -// } + // // @@ -314,11 +309,7 @@ struct HttpHandler { } impl Handler for HttpHandler { - type Error - = Error - where - E: Debug; - + type Error = Error; async fn handle<'a, T, const N: usize>( &self, _task_id: impl Display + Copy, @@ -381,6 +372,7 @@ impl Handler for HttpHandler { Method::Post => { let json = match path { "/wifiscan" => Some(wifi_scan(conn).await), + "/set_config" => Some(set_config(conn).await), _ => None, }; match json { @@ -398,7 +390,6 @@ impl Handler for HttpHandler { } Some(code) => code, }; - conn.complete().await?; let response_time = Instant::now().duration_since(start).as_millis(); @@ -407,28 +398,65 @@ impl Handler for HttpHandler { } } -// .fn_handler("/reboot", Method::Post, move |_| { -// BOARD_ACCESS -// .lock() -// .unwrap() -// .board_hal -// .get_esp() -// .set_restart_to_conf(true); -// reboot_now_for_reboot.store(true, std::sync::atomic::Ordering::Relaxed); -// anyhow::Ok(()) - -// fn wifi_scan( -// _request: &mut Request<&mut EspHttpConnection>, +// fn set_config( +// request: &mut Request<&mut EspHttpConnection>, // ) -> Result, anyhow::Error> { -// let mut board = BOARD_ACCESS.lock().unwrap(); -// let scan_result = board.board_hal.get_esp().wifi_scan()?; -// let mut ssids: Vec<&String<32>> = Vec::new(); -// scan_result.iter().for_each(|s| ssids.push(&s.ssid)); -// let ssid_json = serde_json::to_string(&SSIDList { ssids })?; -// log::info!("Sending ssid list {}", &ssid_json); -// anyhow::Ok(Some(ssid_json)) +// let all = read_up_to_bytes_from_request(request, Some(4096))?; +// let config: PlantControllerConfig = serde_json::from_slice(&all)?; +// +// let mut board = BOARD_ACCESS.lock().expect("board access"); +// board.board_hal.set_config(config)?; +// anyhow::Ok(Some("saved".to_owned())) // } +async fn set_config( + request: &mut Connection<'_, T, N>, +) -> Result, anyhow::Error> +where + T: Read + Write, +{ + let all = read_up_to_bytes_from_request(request, Some(4096)).await?; + let length = all.len(); + let config: PlantControllerConfig = serde_json::from_slice(&all)?; + + let mut board = BOARD_ACCESS.get().await.lock().await; + board.board_hal.get_esp().save_config(all).await?; + log::info!("Wrote config config {:?} with size {}", config, length); + board.board_hal.set_config(config); + anyhow::Ok(Some("saved".to_string())) +} + +async fn read_up_to_bytes_from_request( + request: &mut Connection<'_, T, N>, + limit: Option, +) -> Result, anyhow::Error> +where + T: Read + Write, +{ + let max_read = limit.unwrap_or(1024); + let mut data_store = Vec::new(); + let mut total_read = 0; + loop { + let mut buf = [0_u8; 64]; + let read = match request.read(&mut buf).await { + Ok(read) => read, + Err(e) => bail!("Error reading request {:?}", e), + }; + if read == 0 { + break; + } + let actual_data = &buf[0..read]; + total_read += read; + if total_read > max_read { + bail!("Request too large {total_read} > {max_read}"); + } + data_store.push(actual_data.to_owned()); + } + let allvec = data_store.concat(); + log::info!("Raw data {}", from_utf8(&allvec)?); + Ok(allvec) +} + async fn wifi_scan( _request: &mut Connection<'_, T, N>, ) -> Result, anyhow::Error> { @@ -463,7 +491,7 @@ async fn list_files( _request: &mut Connection<'_, T, N>, ) -> Result, anyhow::Error> { let mut board = BOARD_ACCESS.get().await.lock().await; - let result = board.board_hal.get_esp().list_files().await; + let result = board.board_hal.get_esp().list_files().await?; let file_list_json = serde_json::to_string(&result)?; anyhow::Ok(Some(file_list_json)) } @@ -537,7 +565,7 @@ pub async fn httpd(reboot_now: Arc, stack: Stack<'static>) { .await .unwrap(); - let mut server: Server<2, 512, 10> = Server::new(); + let mut server: Server<2, 512, 15> = Server::new(); server .run(Some(5000), acceptor, HttpHandler { reboot_now }) .await @@ -637,11 +665,6 @@ pub async fn httpd(reboot_now: Arc, stack: Stack<'static>) { // .unwrap(); // // server - // .fn_handler("/set_config", Method::Post, move |request| { - // handle_error_to500(request, set_config) - // }) - // .unwrap(); - // server // .fn_handler("/backup_config", Method::Post, move |request| { // handle_error_to500(request, backup_config) // }) @@ -817,30 +840,6 @@ pub async fn httpd(reboot_now: Arc, stack: Stack<'static>) { //server } // -// fn read_up_to_bytes_from_request( -// request: &mut Request<&mut EspHttpConnection<'_>>, -// limit: Option, -// ) -> Result, anyhow::Error> { -// let max_read = limit.unwrap_or(1024); -// let mut data_store = Vec::new(); -// let mut total_read = 0; -// loop { -// let mut buf = [0_u8; 64]; -// let read = request.read(&mut buf)?; -// if read == 0 { -// break; -// } -// let actual_data = &buf[0..read]; -// total_read += read; -// if total_read > max_read { -// bail!("Request too large {total_read} > {max_read}"); -// } -// data_store.push(actual_data.to_owned()); -// } -// let allvec = data_store.concat(); -// log::info!("Raw data {}", from_utf8(&allvec)?); -// Ok(allvec) -// } async fn handle_json<'a, T, const N: usize>( conn: &mut Connection<'a, T, N>,