From c61a586595b2e03d9a4c696210543cf08701deb6 Mon Sep 17 00:00:00 2001 From: Empire Phoenix Date: Sun, 15 Mar 2026 19:57:19 +0100 Subject: [PATCH] new ota logic --- Software/MainBoard/rust/.idea/vcs.xml | 1 + Software/MainBoard/rust/Cargo.toml | 4 +- Software/MainBoard/rust/src/fat_error.rs | 22 ++- Software/MainBoard/rust/src/hal/esp.rs | 72 +------ Software/MainBoard/rust/src/hal/mod.rs | 182 +++--------------- .../MainBoard/rust/src/hal/shared_flash.rs | 45 ++++- Software/MainBoard/rust/src/hal/v4_hal.rs | 44 +++-- Software/MainBoard/rust/src/main.rs | 133 +++++++------ Software/MainBoard/rust/src/plant_state.rs | 24 +-- .../MainBoard/rust/src/webserver/get_json.rs | 10 +- Software/MainBoard/rust/src/webserver/ota.rs | 45 +++-- .../MainBoard/rust/src/webserver/post_json.rs | 7 +- .../MainBoard/rust/src_webpack/src/api.ts | 3 +- .../MainBoard/rust/src_webpack/src/main.ts | 111 +++++++---- .../MainBoard/rust/src_webpack/src/ota.ts | 10 +- 15 files changed, 313 insertions(+), 400 deletions(-) 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 5e6bbe0..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:?}"), } } } @@ -323,13 +328,18 @@ impl From for FatError { } } - -impl From for FatError{ +impl From for FatError { fn from(value: BmsProtocolError) -> Self { match value { - BmsProtocolError::I2cCommunicationError => { - FatError::String{error: "I2C communication error".to_string()} - } + BmsProtocolError::I2cCommunicationError => FatError::String { + error: "I2C communication error".to_string(), + }, } } -} \ No newline at end of file +} + +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 03e2fb6..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,11 +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::Blocking; use esp_hal::gpio::{Input, RtcPinWithResistors}; use esp_hal::rng::Rng; use esp_hal::rtc_cntl::{ @@ -34,6 +29,8 @@ 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 @@ -155,7 +147,6 @@ macro_rules! mk_static { } impl Esp<'_> { - pub(crate) async fn read_serial_line(&mut self) -> FatResult> { let mut buf = [0u8; 1]; let mut line = String::new(); @@ -171,7 +162,7 @@ impl Esp<'_> { } line.push(c); } - Err(error ) => { + Err(error) => { if line.is_empty() { return Ok(None); } else { @@ -250,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() } @@ -594,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 034c018..5eb76d7 100644 --- a/Software/MainBoard/rust/src/hal/mod.rs +++ b/Software/MainBoard/rust/src/hal/mod.rs @@ -1,5 +1,5 @@ +use esp_hal::uart::Config as UartConfig; use lib_bms_protocol::BmsReadable; -use esp_hal::uart::{Config as UartConfig}; pub(crate) mod battery; // mod can_api; // replaced by external canapi crate pub mod esp; @@ -9,7 +9,6 @@ mod shared_flash; mod v4_hal; mod water; -use lib_bms_protocol::ProtocolVersion; use crate::alloc::string::ToString; use crate::hal::rtc::{DS3231Module, RTCModuleInteraction}; use esp_hal::peripherals::Peripherals; @@ -36,6 +35,7 @@ use esp_hal::peripherals::GPIO6; use esp_hal::peripherals::GPIO7; use esp_hal::peripherals::GPIO8; use esp_hal::peripherals::TWAI0; +use lib_bms_protocol::ProtocolVersion; use crate::{ bail, @@ -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::hal::battery::{WCHI2CSlave}; +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; @@ -93,8 +89,9 @@ 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_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,12 +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>; @@ -169,7 +160,7 @@ 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"); } @@ -324,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( @@ -401,8 +356,8 @@ impl PlantHal { lfs2Filesystem::mount(alloc, lfs2filesystem).expect("Could not mount lfs2 filesystem"), )); - let uart0 = Uart::new(peripherals.UART0, UartConfig::default()) - .map_err(|_| FatError::String { + let uart0 = + Uart::new(peripherals.UART0, UartConfig::default()).map_err(|_| FatError::String { error: "Uart creation failed".to_string(), })?; @@ -417,11 +372,7 @@ impl PlantHal { boot_button, wake_gpio1, ota, - ota_target, - current: running, - slot0_state: state_0, - slot1_state: state_1, - uart0 + uart0, }; //init,reset rtc memory depending on cause @@ -553,7 +504,7 @@ impl PlantHal { //BoardVersion::V4 => { v4_hal::create_v4(free_pins, esp, config, battery_interaction, rtc_module) .await?; - //} + //} //}; HAL { board_hal } @@ -571,9 +522,14 @@ impl PlantHal { ) .await; HAL { - board_hal: v4_hal::create_v4(free_pins, esp, PlantControllerConfig::default(), - Box::new(NoBatteryMonitor {}), rtc_module) - .await? + board_hal: v4_hal::create_v4( + free_pins, + esp, + PlantControllerConfig::default(), + Box::new(NoBatteryMonitor {}), + rtc_module, + ) + .await?, } } }; @@ -582,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() @@ -699,4 +574,3 @@ pub struct DetectionSensorResult { sensor_a: bool, sensor_b: bool, } - 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 1ff4076..89b8c70 100644 --- a/Software/MainBoard/rust/src/hal/v4_hal.rs +++ b/Software/MainBoard/rust/src/hal/v4_hal.rs @@ -11,7 +11,7 @@ use crate::hal::{ }; use crate::log::{LogMessage, LOG_ACCESS}; use alloc::boxed::Box; -use alloc::string::{ToString}; +use alloc::string::ToString; use async_trait::async_trait; use canapi::id::{classify, plant_id, MessageKind, IDENTIFY_CMD_OFFSET}; use canapi::SensorSlot; @@ -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}; @@ -133,10 +132,9 @@ pub struct V4<'a> { extra1: Output<'a>, extra2: Output<'a>, - twai_config: Option> + twai_config: Option>, } - pub(crate) async fn create_v4( peripherals: FreePeripherals<'static>, esp: Esp<'static>, @@ -261,12 +259,11 @@ pub(crate) async fn create_v4( extra1, extra2, can_power, - twai_config + twai_config, }; Ok(Box::new(v)) } - #[async_trait(?Send)] impl<'a> BoardInteraction<'a> for V4<'a> { fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError> { @@ -393,11 +390,18 @@ impl<'a> BoardInteraction<'a> for V4<'a> { if !detect { continue; } - let target = - StandardId::new(plant_id(IDENTIFY_CMD_OFFSET, sensor.into(), (plant +1) as u16)) - .context(">> Could not create address for sensor! (plant: {}) <<")?; + let target = StandardId::new(plant_id( + IDENTIFY_CMD_OFFSET, + sensor.into(), + (plant + 1) as u16, + )) + .context(">> Could not create address for sensor! (plant: {}) <<")?; let can_buffer = [0_u8; 0]; - info!("Sending test message to plant {} sensor {sensor:?} with id {}", plant +1, target.as_raw()); + info!( + "Sending test message to plant {} sensor {sensor:?} with id {}", + plant + 1, + target.as_raw() + ); if let Some(frame) = EspTwaiFrame::new(target, &can_buffer) { // Try a few times; we intentionally ignore rx here and rely on stub logic let resu = twai @@ -405,12 +409,12 @@ impl<'a> BoardInteraction<'a> for V4<'a> { .with_timeout(Duration::from_millis(3000)) .await; match resu { - Ok(_) => { - } + Ok(_) => {} Err(err) => { info!( - "Error sending test message to plant {} sensor {sensor:?}: {err:?}", plant +1 - ); + "Error sending test message to plant {} sensor {sensor:?}: {err:?}", + plant + 1 + ); } } } else { @@ -424,20 +428,17 @@ impl<'a> BoardInteraction<'a> for V4<'a> { .with_timeout(Duration::from_millis(3000)) .await; - let config = twai.stop().into_blocking(); self.twai_config.replace(config); self.can_power.set_low(); - let result = moistures.into(); info!("Autodetection result: {result:?}"); Ok(result) } - async fn general_fault(&mut self, enable: bool) { hold_disable(23); self.general_fault.set_level(enable.into()); @@ -480,7 +481,7 @@ impl<'a> BoardInteraction<'a> for V4<'a> { LOG_ACCESS .lock() .await - .log(LogMessage::TestSensor, a, b, &(plant+1).to_string(), "") + .log(LogMessage::TestSensor, a, b, &(plant + 1).to_string(), "") .await; } Timer::after_millis(10).await; @@ -509,7 +510,6 @@ impl<'a> BoardInteraction<'a> for V4<'a> { } } - async fn wait_for_can_measurements( as_async: &mut Twai<'_, Async>, moistures: &mut Moistures, @@ -536,10 +536,12 @@ async fn wait_for_can_measurements( let frequency = u32::from_be_bytes(bytes); match sensor { SensorSlot::A => { - moistures.sensor_a_hz[plant-1] = Some(frequency as f32); + moistures.sensor_a_hz[plant - 1] = + Some(frequency as f32); } SensorSlot::B => { - moistures.sensor_b_hz[plant-1] = Some(frequency as f32); + moistures.sensor_b_hz[plant - 1] = + Some(frequency as f32); } } } else { diff --git a/Software/MainBoard/rust/src/main.rs b/Software/MainBoard/rust/src/main.rs index 5600601..1891ea5 100644 --- a/Software/MainBoard/rust/src/main.rs +++ b/Software/MainBoard/rust/src/main.rs @@ -29,8 +29,8 @@ use ::log::{error, info, warn}; use alloc::borrow::ToOwned; use alloc::string::{String, ToString}; use alloc::sync::Arc; -use alloc::{format, vec}; use alloc::vec::Vec; +use alloc::{format, vec}; use chrono::{DateTime, Datelike, Timelike, Utc}; use chrono_tz::Tz::{self, UTC}; use core::sync::atomic::{AtomicBool, Ordering}; @@ -42,6 +42,7 @@ use embassy_sync::once_lock::OnceLock; use embassy_time::{Duration, 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; @@ -501,16 +502,17 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { info!("state of charg"); let is_day = board.board_hal.is_day(); - let battery_state = board.board_hal.get_battery_monitor().get_state().await.unwrap_or(BatteryState::Unknown); + let battery_state = board + .board_hal + .get_battery_monitor() + .get_state() + .await + .unwrap_or(BatteryState::Unknown); info!("Battery state is {battery_state:?}"); let state_of_charge = match &battery_state { - BatteryState::Unknown => { - 0 - } - BatteryState::Info(data) => { - data.state_of_charge - } + BatteryState::Unknown => 0, + BatteryState::Info(data) => data.state_of_charge, }; let mut light_state = LightState { @@ -529,22 +531,10 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> { board.board_hal.get_config().night_lamp.night_lamp_hour_end, ); - if state_of_charge - < board - .board_hal - .get_config() - .night_lamp - .low_soc_cutoff - { + if state_of_charge < board.board_hal.get_config().night_lamp.low_soc_cutoff { board.board_hal.get_esp().set_low_voltage_in_cycle(); info!("Set low voltage in cycle"); - } else if state_of_charge - > board - .board_hal - .get_config() - .night_lamp - .low_soc_restore - { + } else if state_of_charge > board.board_hal.get_config().night_lamp.low_soc_restore { board.board_hal.get_esp().clear_low_voltage_in_cycle(); info!("Clear low voltage in cycle"); } @@ -758,7 +748,7 @@ pub async fn do_secure_pump( async fn update_charge_indicator( board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>, -) -> FatResult<()>{ +) -> 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 let current = board.board_hal.get_mptt_current().await?; @@ -766,10 +756,9 @@ async fn update_charge_indicator( .board_hal .set_charge_indicator(current.as_milliamperes() > 20_f64) .await?; - Ok(()) + Ok(()) } - async fn publish_tank_state( board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, tank_state: &TankState, @@ -949,11 +938,7 @@ async fn publish_mppt_state( async fn publish_battery_state( board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>, ) -> () { - let state = board - .board_hal - .get_battery_monitor() - .get_state() - .await; + let state = board.board_hal.get_battery_monitor().get_state().await; let value = match state { Ok(state) => { let json = serde_json::to_string(&state).unwrap().to_owned(); @@ -986,7 +971,7 @@ async fn wait_infinity( loop { { let mut board = BOARD_ACCESS.get().await.lock().await; - match update_charge_indicator(&mut board).await{ + match update_charge_indicator(&mut board).await { Ok(_) => {} Err(error) => { if !suppress_further_mppt_error { @@ -1069,35 +1054,38 @@ async fn wait_infinity( } } -async fn handle_serial_config(board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'_>>, serial_config_receive: &AtomicBool, reboot_now: &AtomicBool) -> FatResult<()> { +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(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(()) } - 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(()) @@ -1153,13 +1141,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), } } @@ -1168,6 +1178,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/plant_state.rs b/Software/MainBoard/rust/src/plant_state.rs index 0e92eee..26f914b 100644 --- a/Software/MainBoard/rust/src/plant_state.rs +++ b/Software/MainBoard/rust/src/plant_state.rs @@ -16,7 +16,6 @@ pub enum MoistureSensorError { #[derive(Debug, PartialEq, Serialize)] pub enum MoistureSensorState { - Disabled, MoistureValue { raw_hz: f32, moisture_percent: f32 }, SensorError(MoistureSensorError), } @@ -117,12 +116,11 @@ impl PlantState { plant_id: usize, board: &mut HAL<'_>, ) -> Self { - let sensor_a = { //if board.board_hal.get_config().plants[plant_id].sensor_a { + let sensor_a = { + //if board.board_hal.get_config().plants[plant_id].sensor_a { let raw = moistures.sensor_a_hz[plant_id]; match raw { - None => { - MoistureSensorState::SensorError(MoistureSensorError::NoMessage) - } + None => MoistureSensorState::SensorError(MoistureSensorError::NoMessage), Some(raw) => { match map_range_moisture( raw, @@ -141,17 +139,15 @@ impl PlantState { } } } - }; // else { - // MoistureSensorState::Disabled - //}; + // MoistureSensorState::Disabled + //}; - let sensor_b = { //if board.board_hal.get_config().plants[plant_id].sensor_b { + let sensor_b = { + //if board.board_hal.get_config().plants[plant_id].sensor_b { let raw = moistures.sensor_b_hz[plant_id]; match raw { - None => { - MoistureSensorState::SensorError(MoistureSensorError::NoMessage) - } + None => MoistureSensorState::SensorError(MoistureSensorError::NoMessage), Some(raw) => { match map_range_moisture( raw, @@ -171,8 +167,8 @@ impl PlantState { } } }; // else { - // MoistureSensorState::Disabled - //}; + // MoistureSensorState::Disabled + //}; let previous_pump = board.board_hal.get_esp().last_pump_time(plant_id); let consecutive_pump_count = board.board_hal.get_esp().consecutive_pump_count(plant_id); diff --git a/Software/MainBoard/rust/src/webserver/get_json.rs b/Software/MainBoard/rust/src/webserver/get_json.rs index e801b35..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; @@ -43,7 +43,6 @@ where plant_state.push(PlantState::read_hardware_state(moistures, i, &mut board).await); } let a = Vec::from_iter(plant_state.iter().map(|s| match &s.sensor_a { - MoistureSensorState::Disabled => "disabled".to_string(), MoistureSensorState::MoistureValue { raw_hz, moisture_percent, @@ -53,7 +52,6 @@ where MoistureSensorState::SensorError(err) => format!("{err:?}"), })); let b = Vec::from_iter(plant_state.iter().map(|s| match &s.sensor_b { - MoistureSensorState::Disabled => "disabled".to_string(), MoistureSensorState::MoistureValue { raw_hz, moisture_percent, @@ -128,11 +126,7 @@ pub(crate) async fn get_battery_state( _request: &mut Connection<'_, T, N>, ) -> FatResult> { let mut board = BOARD_ACCESS.get().await.lock().await; - let battery_state = board - .board_hal - .get_battery_monitor() - .get_state() - .await?; + let battery_state = board.board_hal.get_battery_monitor().get_state().await?; Ok(Some(serde_json::to_string(&battery_state)?)) } 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 e49cac2..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}; @@ -142,9 +142,6 @@ where board.board_hal.can_power(can_power_request.state).await?; let enable = can_power_request.state; - info!( - "set can power to {enable}" - ); + info!("set can power to {enable}"); Ok(None) } - 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