diff --git a/Software/MainBoard/rust/.idea/vcs.xml b/Software/MainBoard/rust/.idea/vcs.xml index c2365ab..298f634 100644 --- a/Software/MainBoard/rust/.idea/vcs.xml +++ b/Software/MainBoard/rust/.idea/vcs.xml @@ -2,5 +2,6 @@ + \ No newline at end of file diff --git a/Software/MainBoard/rust/Cargo.toml b/Software/MainBoard/rust/Cargo.toml index 34e4b80..12ecede 100644 --- a/Software/MainBoard/rust/Cargo.toml +++ b/Software/MainBoard/rust/Cargo.toml @@ -51,6 +51,8 @@ esp-storage = { version = "0.8.1", features = ["esp32c6"] } esp-radio = { version = "0.17.0", features = ["esp32c6", "log-04", "smoltcp", "wifi", "unstable"] } esp-alloc = { version = "0.9.0", features = ["esp32c6", "internal-heap-stats"] } +esp-hal-ota = { version = "0.4.6", features = ["esp32c6"] } + # Async runtime (Embassy core) embassy-executor = { version = "0.9.1", features = ["log", "nightly"] } embassy-time = { version = "0.5.0", features = ["log"], default-features = false } @@ -95,7 +97,7 @@ embedded-can = "0.4.1" nb = "1.1.0" # Concrete hardware drivers and sensors/IO expanders -lib-bms-protocol = {git = "https://gitea.wlandt.de/judge/ch32-bms.git" , default-features = false } +lib-bms-protocol = { git = "https://gitea.wlandt.de/judge/ch32-bms.git", default-features = false } onewire = "0.4.0" ds323x = "0.7.0" eeprom24x = "0.7.2" diff --git a/Software/MainBoard/rust/src/fat_error.rs b/Software/MainBoard/rust/src/fat_error.rs index 9bbb6cb..b6a70cf 100644 --- a/Software/MainBoard/rust/src/fat_error.rs +++ b/Software/MainBoard/rust/src/fat_error.rs @@ -11,6 +11,7 @@ use embedded_storage::nor_flash::NorFlashErrorKind; use esp_hal::i2c::master::ConfigError; use esp_hal::pcnt::unit::{InvalidHighLimit, InvalidLowLimit}; use esp_hal::twai::EspTwaiError; +use esp_hal_ota::OtaError; use esp_radio::wifi::WifiError; use ina219::errors::{BusVoltageReadError, ShuntVoltageReadError}; use lib_bms_protocol::BmsProtocolError; @@ -73,6 +74,9 @@ pub enum FatError { SNTPError { error: sntpc::Error, }, + OtaError { + error: OtaError, + }, } pub type FatResult = Result; @@ -106,6 +110,7 @@ impl fmt::Display for FatError { } FatError::SNTPError { error } => write!(f, "SNTPError {error:?}"), FatError::BMSError { error } => write!(f, "BMSError, {error}"), + FatError::OtaError { error } => write!(f, "OtaError {error:?}"), } } } @@ -332,3 +337,9 @@ impl From for FatError { } } } + +impl From for FatError { + fn from(value: OtaError) -> Self { + FatError::OtaError { error: value } + } +} diff --git a/Software/MainBoard/rust/src/hal/esp.rs b/Software/MainBoard/rust/src/hal/esp.rs index 4c89ac9..6f576dd 100644 --- a/Software/MainBoard/rust/src/hal/esp.rs +++ b/Software/MainBoard/rust/src/hal/esp.rs @@ -10,7 +10,7 @@ use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem; use crate::hal::shared_flash::MutexFlashStorage; use alloc::string::ToString; use alloc::sync::Arc; -use alloc::{format, string::String, vec, vec::Vec}; +use alloc::{format, string::String, vec::Vec}; use core::net::{IpAddr, Ipv4Addr, SocketAddr}; use core::str::FromStr; use core::sync::atomic::Ordering; @@ -21,10 +21,6 @@ use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::mutex::{Mutex, MutexGuard}; use embassy_sync::once_lock::OnceLock; use embassy_time::{Duration, Timer, WithTimeout}; -use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash}; -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::{ @@ -34,6 +30,7 @@ use esp_hal::rtc_cntl::{ use esp_hal::system::software_reset; use esp_hal::uart::Uart; use esp_hal::Blocking; +use esp_hal_ota::Ota; use esp_println::println; use esp_radio::wifi::{ AccessPointConfig, AccessPointInfo, AuthMethod, ClientConfig, ModeConfig, ScanConfig, @@ -129,12 +126,7 @@ pub struct Esp<'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 ota: Ota<'static, MutexFlashStorage>, - pub ota_target: &'static mut FlashRegion<'static, MutexFlashStorage>, - pub current: AppPartitionSubType, - pub slot0_state: OtaImageState, - pub slot1_state: OtaImageState, + pub ota: &'static mut Ota<&'static mut MutexFlashStorage>, } // SAFETY: On this target we never move Esp across OS threads; the firmware runs single-core @@ -249,50 +241,6 @@ impl Esp<'_> { 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); - 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:?}"); - //determine nextslot crc - - self.set_restart_to_conf(true); - Ok(()) - } - pub(crate) fn mode_override_pressed(&mut self) -> bool { self.boot_button.is_low() } @@ -593,14 +541,7 @@ impl Esp<'_> { duration_in_ms: u64, mut rtc: MutexGuard, ) -> ! { - // 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 { - self.ota - .set_current_ota_state(Valid) - .expect("Could not set image to valid"); - } - } + //TODO HERE Mark the current OTA image as valid if we reached here while in pending verify. if duration_in_ms == 0 { software_reset(); diff --git a/Software/MainBoard/rust/src/hal/mod.rs b/Software/MainBoard/rust/src/hal/mod.rs index 2107d42..5eb76d7 100644 --- a/Software/MainBoard/rust/src/hal/mod.rs +++ b/Software/MainBoard/rust/src/hal/mod.rs @@ -65,24 +65,20 @@ 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_bootloader_esp_idf::partitions::{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::fat_error::{FatError, FatResult}; use crate::hal::battery::WCHI2CSlave; 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::ReadStorage; use esp_alloc as _; use esp_backtrace as _; -use esp_bootloader_esp_idf::ota::{Ota, OtaImageState}; use esp_hal::delay::Delay; use esp_hal::i2c::master::{BusTimeout, Config, I2c}; use esp_hal::interrupt::software::SoftwareInterruptControl; @@ -95,6 +91,7 @@ use esp_hal::time::Rate; use esp_hal::timer::timg::TimerGroup; use esp_hal::uart::Uart; use esp_hal::Blocking; +use esp_hal_ota::Ota; use esp_radio::{init, Controller}; use esp_storage::FlashStorage; use littlefs2::fs::{Allocation, Filesystem as lfs2Filesystem}; @@ -138,11 +135,6 @@ pub struct HAL<'a> { pub board_hal: Box + Send>, } -pub struct DetectionRequest { - pub sensorsa: [Sensor; PLANT_COUNT], - pub sensorsb: [Sensor; PLANT_COUNT], -} - #[async_trait(?Send)] pub trait BoardInteraction<'a> { fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError>; @@ -168,7 +160,9 @@ pub trait BoardInteraction<'a> { async fn can_power(&mut self, state: bool) -> FatResult<()>; // Return JSON string with autodetected sensors per plant. Default: not supported. - async fn detect_sensors(&mut self, request: Detection) -> FatResult; + async fn detect_sensors(&mut self, _request: Detection) -> FatResult { + 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 @@ -321,51 +315,15 @@ impl PlantHal { let pt = esp_bootloader_esp_idf::partitions::read_partition_table(flash_storage, 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, - ota_data.as_embedded_storage(flash_storage_2) - ); - - let state_0 = ota_state(AppPartitionSubType::Ota0, ota_data); - let state_1 = ota_state(AppPartitionSubType::Ota1, ota_data); - let mut ota = Ota::new(ota_data, 2)?; - let running = get_current_slot_and_fix_ota_data(&mut ota, state_0, state_1)?; - let target = next_partition(running)?; - - info!("Currently running OTA slot: {running:?}"); - info!("Slot0 state: {state_0:?}"); - info!("Slot1 state: {state_1:?}"); - - //get current_state and next_state here! - let ota_target = match target { - AppPartitionSubType::Ota0 => pt - .find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App( - AppPartitionSubType::Ota0, - ))? - .context("Partition table invalid no ota0")?, - AppPartitionSubType::Ota1 => pt - .find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App( - AppPartitionSubType::Ota1, - ))? - .context("Partition table invalid no ota1")?, - _ => { - bail!("Invalid target partition"); - } - }; - - let ota_target = mk_static!(PartitionEntry, ota_target); - let ota_target = mk_static!( - FlashRegion, - ota_target.as_embedded_storage(flash_storage) + let ota = mk_static!( + Ota<&mut MutexFlashStorage>, + Ota::new(flash_storage_2).unwrap() ); + info!("Ota initialized"); + let selected = ota.get_currently_booted_partition(); + let ota_state = ota.get_ota_image_state(); + info!("Currently running partition slot: {selected:?}"); + info!("Slot state: {ota_state:?}"); let data_partition = pt .find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data( @@ -414,10 +372,6 @@ impl PlantHal { boot_button, wake_gpio1, ota, - ota_target, - current: running, - slot0_state: state_0, - slot1_state: state_1, uart0, }; @@ -584,87 +538,6 @@ impl PlantHal { } } -fn ota_state( - slot: AppPartitionSubType, - ota_data: &mut FlashRegion, -) -> 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 - let mut slot_buf = [0u8; 32]; - if slot == AppPartitionSubType::Ota0 { - let _ = ReadStorage::read(ota_data, 0x0000, &mut slot_buf); - } else { - let _ = ReadStorage::read(ota_data, 0x1000, &mut slot_buf); - } - let raw_state = u32::from_le_bytes(slot_buf[24..28].try_into().unwrap_or([0xff; 4])); - - OtaImageState::try_from(raw_state).unwrap_or(OtaImageState::Undefined) -} - -fn get_current_slot_and_fix_ota_data( - ota: &mut Ota, - state0: OtaImageState, - state1: OtaImageState, -) -> Result { - 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_app_partition()?; - if swap { - let other = match current { - AppPartitionSubType::Ota0 => state1, - AppPartitionSubType::Ota1 => 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 {state:?} other state has {other:?} swapping"); - let next = next_partition(current)?; - ota.set_current_app_partition(next)?; - - //we actually booted other slot, than partition table assumes - return Ok(ota.current_app_partition()?); - }; - Ok(current) -} - -pub fn next_partition(current: AppPartitionSubType) -> FatResult { - let next = match current { - AppPartitionSubType::Ota0 => AppPartitionSubType::Ota1, - AppPartitionSubType::Ota1 => AppPartitionSubType::Ota0, - _ => { - bail!("Current slot is not ota0 or ota1"); - } - }; - Ok(next) -} - pub async fn esp_time() -> DateTime { let guard = TIME_ACCESS.get().await.lock().await; DateTime::from_timestamp_micros(guard.current_time_us() as i64).unwrap() diff --git a/Software/MainBoard/rust/src/hal/shared_flash.rs b/Software/MainBoard/rust/src/hal/shared_flash.rs index b206ea5..cef76b3 100644 --- a/Software/MainBoard/rust/src/hal/shared_flash.rs +++ b/Software/MainBoard/rust/src/hal/shared_flash.rs @@ -3,7 +3,7 @@ use core::cell::RefCell; use core::ops::{Deref, DerefMut}; use embassy_sync::blocking_mutex::CriticalSectionMutex; use embedded_storage::nor_flash::{ErrorType, NorFlash, ReadNorFlash}; -use embedded_storage::ReadStorage; +use embedded_storage::{ReadStorage, Storage}; use esp_storage::{FlashStorage, FlashStorageError}; #[derive(Clone)] @@ -11,6 +11,19 @@ pub struct MutexFlashStorage { pub(crate) inner: Arc>>>, } +impl Storage for MutexFlashStorage { + fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> { + self.inner + .lock(|f| NorFlash::write(f.borrow_mut().deref_mut(), offset, bytes)) + } +} + +impl Storage for &mut MutexFlashStorage { + fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> { + Storage::write(&mut (**self), offset, bytes) + } +} + impl ReadStorage for MutexFlashStorage { type Error = FlashStorageError; @@ -25,9 +38,29 @@ impl ReadStorage for MutexFlashStorage { } } -impl embedded_storage::Storage for MutexFlashStorage { - fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> { - NorFlash::write(self, offset, bytes) +impl ReadStorage for &mut MutexFlashStorage { + type Error = FlashStorageError; + + fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), FlashStorageError> { + ReadStorage::read(&mut (**self), offset, bytes) + } + + fn capacity(&self) -> usize { + ReadStorage::capacity(&(**self)) + } +} + +impl ReadStorage for &MutexFlashStorage { + type Error = FlashStorageError; + + fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), FlashStorageError> { + self.inner + .lock(|f| ReadStorage::read(f.borrow_mut().deref_mut(), offset, bytes)) + } + + fn capacity(&self) -> usize { + self.inner + .lock(|f| ReadStorage::capacity(f.borrow().deref())) } } @@ -35,6 +68,10 @@ impl ErrorType for MutexFlashStorage { type Error = FlashStorageError; } +impl ErrorType for &MutexFlashStorage { + type Error = FlashStorageError; +} + impl ReadNorFlash for MutexFlashStorage { const READ_SIZE: usize = 1; diff --git a/Software/MainBoard/rust/src/hal/v4_hal.rs b/Software/MainBoard/rust/src/hal/v4_hal.rs index ff8d87c..0d20d4f 100644 --- a/Software/MainBoard/rust/src/hal/v4_hal.rs +++ b/Software/MainBoard/rust/src/hal/v4_hal.rs @@ -23,7 +23,6 @@ use esp_hal::gpio::{Flex, Input, InputConfig, Level, Output, OutputConfig, Pull} use esp_hal::i2c::master::I2c; use esp_hal::twai::{EspTwaiError, EspTwaiFrame, StandardId, Twai, TwaiConfiguration, TwaiMode}; use esp_hal::{twai, Async, Blocking}; -use esp_println::println; use ina219::address::{Address, Pin}; use ina219::calibration::UnCalibrated; use ina219::configuration::{Configuration, OperatingMode, Resolution}; diff --git a/Software/MainBoard/rust/src/main.rs b/Software/MainBoard/rust/src/main.rs index af2942f..96ff5e3 100644 --- a/Software/MainBoard/rust/src/main.rs +++ b/Software/MainBoard/rust/src/main.rs @@ -42,6 +42,7 @@ use embassy_sync::once_lock::OnceLock; use embassy_time::{Duration, Instant, Timer, WithTimeout}; use esp_hal::rom::ets_delay_us; use esp_hal::system::software_reset; +use esp_hal_ota::OtaImgState; use esp_println::{logger, println}; use hal::battery::BatteryState; use log::LogMessage; @@ -1242,13 +1243,35 @@ async fn get_version( let branch = env!("VERGEN_GIT_BRANCH").to_owned(); let hash = &env!("VERGEN_GIT_SHA")[0..8]; - let board = board.board_hal.get_esp(); + let esp = board.board_hal.get_esp(); + let current_partition: String = match esp.ota.get_currently_booted_partition() { + Some(partition) => match partition { + 0 => "OTA0".to_string(), + 1 => "OTA1".to_string(), + _ => format!("Pentry: {:?}", partition), + }, + None => "Missing otainfo".to_string(), + }; + let state = esp.ota.get_ota_image_state(); + let current_state: String = match state { + Ok(state) => match state { + OtaImgState::EspOtaImgNew => "New".to_string(), + OtaImgState::EspOtaImgPendingVerify => "PendingVerify".to_string(), + OtaImgState::EspOtaImgValid => "Valid".to_string(), + OtaImgState::EspOtaImgInvalid => "Invalid".to_string(), + OtaImgState::EspOtaImgAborted => "Aborted".to_string(), + OtaImgState::EspOtaImgUndefined => "Undefined".to_string(), + }, + Err(err) => { + format!("Error: {:?}", err) + } + }; + VersionInfo { git_hash: branch + "@" + hash, build_time: env!("VERGEN_BUILD_TIMESTAMP").to_owned(), - current: format!("{:?}", board.current), - slot0_state: format!("{:?}", board.slot0_state), - slot1_state: format!("{:?}", board.slot1_state), + current: format!("{:?}", current_partition), + state: format!("{:?}", current_state), } } @@ -1257,6 +1280,5 @@ struct VersionInfo { git_hash: String, build_time: String, current: String, - slot0_state: String, - slot1_state: String, + state: String, } diff --git a/Software/MainBoard/rust/src/webserver/get_json.rs b/Software/MainBoard/rust/src/webserver/get_json.rs index d34edcc..107ee90 100644 --- a/Software/MainBoard/rust/src/webserver/get_json.rs +++ b/Software/MainBoard/rust/src/webserver/get_json.rs @@ -5,7 +5,7 @@ use crate::plant_state::{MoistureSensorState, PlantState}; use crate::tank::determine_tank_state; use crate::{get_version, BOARD_ACCESS}; use alloc::format; -use alloc::string::{String, ToString}; +use alloc::string::String; use alloc::vec::Vec; use chrono_tz::Tz; use core::str::FromStr; diff --git a/Software/MainBoard/rust/src/webserver/ota.rs b/Software/MainBoard/rust/src/webserver/ota.rs index c8195df..0a22122 100644 --- a/Software/MainBoard/rust/src/webserver/ota.rs +++ b/Software/MainBoard/rust/src/webserver/ota.rs @@ -4,7 +4,8 @@ use crate::BOARD_ACCESS; use edge_http::io::server::Connection; use edge_http::Method; use edge_nal::io::{Read, Write}; -use log::info; +use esp_hal_ota::OtaError; +use log::{error, info}; pub(crate) async fn ota_operations( conn: &mut Connection<'_, T, { N }>, @@ -28,27 +29,45 @@ where Some(200) } Method::Post => { - let mut offset = 0_usize; + let size = read_up_to_bytes_from_request(conn, Some(4)).await?; + let flash_size = u32::from_le_bytes(size[..4].try_into().unwrap()); + info!("flash size: {flash_size}"); + let crc32 = read_up_to_bytes_from_request(conn, Some(4)).await?; + let target_crc = u32::from_le_bytes(crc32[..4].try_into().unwrap()); + info!("crc32: {target_crc}"); + + BOARD_ACCESS + .get() + .await + .lock() + .await + .board_hal + .get_esp() + .ota + .ota_begin(flash_size, target_crc)?; + let mut chunk = 0; loop { let buf = read_up_to_bytes_from_request(conn, Some(4096)).await?; if buf.is_empty() { - info!("file request for ota finished"); - let mut board = BOARD_ACCESS.get().await.lock().await; - board.board_hal.get_esp().finalize_ota().await?; - break; + error!("Upload finished, but no enough data received."); + return Err(OtaError::WrongCRC)?; } else { let mut board = BOARD_ACCESS.get().await.lock().await; board.board_hal.progress(chunk as u32).await; // Erase next block if we are at a 4K boundary (including the first block at offset 0) - info!("erasing and writing block 0x{offset:x}"); - board - .board_hal - .get_esp() - .write_ota(offset as u32, &buf) - .await?; + let finsihed = board.board_hal.get_esp().ota.ota_write_chunk(&buf)?; + if finsihed { + board.board_hal.get_esp().ota.ota_flush(true, true)?; + + board.board_hal.get_esp().set_restart_to_conf(true); + break; + } + info!( + "Progress: {}%", + (board.board_hal.get_esp().ota.get_ota_progress() * 100.0) as u8 + ); } - offset += buf.len(); chunk += 1; } BOARD_ACCESS diff --git a/Software/MainBoard/rust/src/webserver/post_json.rs b/Software/MainBoard/rust/src/webserver/post_json.rs index d994eb8..c92fa75 100644 --- a/Software/MainBoard/rust/src/webserver/post_json.rs +++ b/Software/MainBoard/rust/src/webserver/post_json.rs @@ -1,6 +1,6 @@ use crate::config::PlantControllerConfig; use crate::fat_error::FatResult; -use crate::hal::{esp_set_time, Detection, DetectionRequest}; +use crate::hal::{esp_set_time, Detection}; use crate::webserver::read_up_to_bytes_from_request; use crate::{do_secure_pump, BOARD_ACCESS}; use alloc::string::{String, ToString}; diff --git a/Software/MainBoard/rust/src_webpack/src/api.ts b/Software/MainBoard/rust/src_webpack/src/api.ts index 876390b..80aeea9 100644 --- a/Software/MainBoard/rust/src_webpack/src/api.ts +++ b/Software/MainBoard/rust/src_webpack/src/api.ts @@ -163,8 +163,7 @@ export interface VersionInfo { git_hash: string, build_time: string, current: string, - slot0_state: string, - slot1_state: string, + state: string } export interface BatteryState { diff --git a/Software/MainBoard/rust/src_webpack/src/main.ts b/Software/MainBoard/rust/src_webpack/src/main.ts index f3dcf9f..a9dc729 100644 --- a/Software/MainBoard/rust/src_webpack/src/main.ts +++ b/Software/MainBoard/rust/src_webpack/src/main.ts @@ -191,36 +191,70 @@ export class Controller { } uploadNewFirmware(file: File) { - let current = 0; - let max = 100; - controller.progressview.addProgress("ota_upload", (current / max) * 100, "Uploading firmeware (" + current + "/" + max + ")") - const ajax = new XMLHttpRequest(); - ajax.upload.addEventListener("progress", event => { - current = event.loaded / 1000; - max = event.total / 1000; + const reader = new FileReader(); + reader.onload = () => { + const arrayBuffer = reader.result as ArrayBuffer; + const data = new Uint8Array(arrayBuffer); + const crc = this.crc32(data); + const size = data.length; + + const payload = new Uint8Array(size + 8); + const view = new DataView(payload.buffer); + view.setUint32(0, size, true); + view.setUint32(4, crc, true); + payload.set(data, 8); + + let current = 0; + let max = 100; controller.progressview.addProgress("ota_upload", (current / max) * 100, "Uploading firmeware (" + current + "/" + max + ")") - }, false); - ajax.addEventListener("load", () => { - controller.progressview.removeProgress("ota_upload") - const status = ajax.status; - if (status >= 200 && status < 300) { - controller.reboot(); - } else { - const statusText = ajax.statusText || ""; - const body = ajax.responseText || ""; - toast.error(`OTA update error (${status}${statusText ? ' ' + statusText : ''}): ${body}`); + const ajax = new XMLHttpRequest(); + ajax.upload.addEventListener("progress", event => { + current = event.loaded / 1000; + max = event.total / 1000; + controller.progressview.addProgress("ota_upload", (current / max) * 100, "Uploading firmeware (" + current + "/" + max + ")") + }, false); + ajax.addEventListener("load", () => { + controller.progressview.removeProgress("ota_upload") + const status = ajax.status; + if (status >= 200 && status < 300) { + controller.reboot(); + } else { + const statusText = ajax.statusText || ""; + const body = ajax.responseText || ""; + toast.error(`OTA update error (${status}${statusText ? ' ' + statusText : ''}): ${body}`); + } + }, false); + ajax.addEventListener("error", () => { + controller.progressview.removeProgress("ota_upload") + toast.error("OTA upload failed due to a network error."); + }, false); + ajax.addEventListener("abort", () => { + controller.progressview.removeProgress("ota_upload") + toast.error("OTA upload was aborted."); + }, false); + ajax.open("POST", PUBLIC_URL + "/ota"); + ajax.send(payload); + }; + reader.onerror = () => { + toast.error("Error reading firmware file."); + }; + reader.readAsArrayBuffer(file); + } + + private crc32(data: Uint8Array): number { + let crc = 0xFFFFFFFF; + for (let i = 0; i < data.length; i++) { + let byte = data[i]; + crc ^= byte; + for (let j = 0; j < 8; j++) { + if (crc & 1) { + crc = (crc >>> 1) ^ 0xEDB88320; + } else { + crc = crc >>> 1; + } } - }, false); - ajax.addEventListener("error", () => { - controller.progressview.removeProgress("ota_upload") - toast.error("OTA upload failed due to a network error."); - }, false); - ajax.addEventListener("abort", () => { - controller.progressview.removeProgress("ota_upload") - toast.error("OTA upload was aborted."); - }, false); - ajax.open("POST", PUBLIC_URL + "/ota"); - ajax.send(file); + } + return (crc ^ 0xFFFFFFFF) >>> 0; } async version(): Promise { @@ -264,12 +298,14 @@ export class Controller { method: "POST", body: json, }) - .then(response => response.text()) - .then(text => statusCallback(text)) - .then( _ => { - controller.progressview.removeProgress("set_config"); - setTimeout(() => { controller.downloadConfig() }, 250) - }) + .then(response => response.text()) + .then(text => statusCallback(text)) + .then(_ => { + controller.progressview.removeProgress("set_config"); + setTimeout(() => { + controller.downloadConfig() + }, 250) + }) } async backupConfig(json: string): Promise { @@ -290,7 +326,6 @@ export class Controller { method: "POST", body: pretty }).then( - _ => controller.progressview.removeProgress("write_rtc") ) } @@ -378,9 +413,9 @@ export class Controller { var pretty = JSON.stringify(detection, undefined, 1); - fetch(PUBLIC_URL + "/detect_sensors", { method: "POST", body: pretty }) + fetch(PUBLIC_URL + "/detect_sensors", {method: "POST", body: pretty}) .then(response => response.json()) - .then (json => json as Detection) + .then(json => json as Detection) .then(json => { clearTimeout(timerId); controller.progressview.removeProgress("detect_sensors"); @@ -531,7 +566,7 @@ export class Controller { private setCanPower(checked: boolean) { var body: CanPower = { - state : checked + state: checked } var pretty = JSON.stringify(body, undefined, 1); diff --git a/Software/MainBoard/rust/src_webpack/src/ota.ts b/Software/MainBoard/rust/src_webpack/src/ota.ts index c407932..dd791ed 100644 --- a/Software/MainBoard/rust/src_webpack/src/ota.ts +++ b/Software/MainBoard/rust/src_webpack/src/ota.ts @@ -6,8 +6,8 @@ export class OTAView { readonly firmware_buildtime: HTMLDivElement; readonly firmware_githash: HTMLDivElement; readonly firmware_partition: HTMLDivElement; - readonly firmware_state0: HTMLDivElement; - readonly firmware_state1: HTMLDivElement; + readonly firmware_state: HTMLDivElement; + constructor(controller: Controller) { (document.getElementById("firmwareview") as HTMLElement).innerHTML = require("./ota.html") @@ -18,8 +18,7 @@ export class OTAView { this.firmware_githash = document.getElementById("firmware_githash") as HTMLDivElement; this.firmware_partition = document.getElementById("firmware_partition") as HTMLDivElement; - this.firmware_state0 = document.getElementById("firmware_state0") as HTMLDivElement; - this.firmware_state1 = document.getElementById("firmware_state1") as HTMLDivElement; + this.firmware_state = document.getElementById("firmware_state") as HTMLDivElement; const file = document.getElementById("firmware_file") as HTMLInputElement; @@ -42,7 +41,6 @@ export class OTAView { this.firmware_buildtime.innerText = versionInfo.build_time; this.firmware_githash.innerText = versionInfo.git_hash; this.firmware_partition.innerText = versionInfo.current; - this.firmware_state0.innerText = versionInfo.slot0_state; - this.firmware_state1.innerText = versionInfo.slot1_state; + this.firmware_state.innerText = versionInfo.state; } } \ No newline at end of file