From 66e1fe63e02a5b58982d45b640757c0a50608cd6 Mon Sep 17 00:00:00 2001 From: Empire Phoenix Date: Tue, 17 Mar 2026 22:17:47 +0100 Subject: [PATCH] Revert "new ota logic" This reverts commit c61a586595b2e03d9a4c696210543cf08701deb6. --- 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 | 26 +-- .../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, 401 insertions(+), 314 deletions(-) diff --git a/Software/MainBoard/rust/.idea/vcs.xml b/Software/MainBoard/rust/.idea/vcs.xml index 298f634..c2365ab 100644 --- a/Software/MainBoard/rust/.idea/vcs.xml +++ b/Software/MainBoard/rust/.idea/vcs.xml @@ -2,6 +2,5 @@ - \ No newline at end of file diff --git a/Software/MainBoard/rust/Cargo.toml b/Software/MainBoard/rust/Cargo.toml index 12ecede..34e4b80 100644 --- a/Software/MainBoard/rust/Cargo.toml +++ b/Software/MainBoard/rust/Cargo.toml @@ -51,8 +51,6 @@ 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 } @@ -97,7 +95,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 b6a70cf..5e6bbe0 100644 --- a/Software/MainBoard/rust/src/fat_error.rs +++ b/Software/MainBoard/rust/src/fat_error.rs @@ -11,7 +11,6 @@ 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; @@ -74,9 +73,6 @@ pub enum FatError { SNTPError { error: sntpc::Error, }, - OtaError { - error: OtaError, - }, } pub type FatResult = Result; @@ -110,7 +106,6 @@ 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:?}"), } } } @@ -328,18 +323,13 @@ 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()} + } } } -} - -impl From for FatError { - fn from(value: OtaError) -> Self { - FatError::OtaError { error: value } - } -} +} \ No newline at end of file diff --git a/Software/MainBoard/rust/src/hal/esp.rs b/Software/MainBoard/rust/src/hal/esp.rs index 6f576dd..03e2fb6 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}; +use alloc::{format, string::String, vec, vec::Vec}; use core::net::{IpAddr, Ipv4Addr, SocketAddr}; use core::str::FromStr; use core::sync::atomic::Ordering; @@ -21,6 +21,11 @@ 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::{ @@ -29,8 +34,6 @@ 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, @@ -126,7 +129,12 @@ 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: &'static mut Ota<&'static mut MutexFlashStorage>, + + pub ota: Ota<'static, 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 @@ -147,6 +155,7 @@ 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(); @@ -162,7 +171,7 @@ impl Esp<'_> { } line.push(c); } - Err(error) => { + Err(error ) => { if line.is_empty() { return Ok(None); } else { @@ -241,6 +250,50 @@ 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() } @@ -541,7 +594,14 @@ impl Esp<'_> { duration_in_ms: u64, mut rtc: MutexGuard, ) -> ! { - //TODO HERE Mark the current OTA image as valid if we reached here while in pending verify. + // 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"); + } + } 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 5eb76d7..034c018 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,6 +9,7 @@ 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; @@ -35,7 +36,6 @@ 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,20 +65,24 @@ 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::{DataPartitionSubType, FlashRegion, PartitionEntry}; +use esp_bootloader_esp_idf::partitions::{ + AppPartitionSubType, DataPartitionSubType, FlashRegion, PartitionEntry, +}; use esp_hal::clock::CpuClock; use esp_hal::gpio::{Input, InputConfig, Pull}; use measurements::{Current, Voltage}; -use crate::fat_error::{FatError, FatResult}; -use crate::hal::battery::WCHI2CSlave; +use crate::fat_error::{ContextExt, 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; @@ -89,9 +93,8 @@ 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::uart::Uart; use esp_hal::Blocking; -use esp_hal_ota::Ota; +use esp_hal::uart::Uart; use esp_radio::{init, Controller}; use esp_storage::FlashStorage; use littlefs2::fs::{Allocation, Filesystem as lfs2Filesystem}; @@ -135,6 +138,12 @@ 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>; @@ -160,7 +169,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"); } @@ -315,15 +324,51 @@ impl PlantHal { let pt = esp_bootloader_esp_idf::partitions::read_partition_table(flash_storage, tablebuffer)?; - let ota = mk_static!( - Ota<&mut MutexFlashStorage>, - Ota::new(flash_storage_2).unwrap() + 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) ); - 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( @@ -356,8 +401,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(), })?; @@ -372,7 +417,11 @@ impl PlantHal { boot_button, wake_gpio1, ota, - uart0, + ota_target, + current: running, + slot0_state: state_0, + slot1_state: state_1, + uart0 }; //init,reset rtc memory depending on cause @@ -504,7 +553,7 @@ impl PlantHal { //BoardVersion::V4 => { v4_hal::create_v4(free_pins, esp, config, battery_interaction, rtc_module) .await?; - //} + //} //}; HAL { board_hal } @@ -522,14 +571,9 @@ 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? } } }; @@ -538,6 +582,87 @@ 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() @@ -574,3 +699,4 @@ 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 cef76b3..b206ea5 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, Storage}; +use embedded_storage::ReadStorage; use esp_storage::{FlashStorage, FlashStorageError}; #[derive(Clone)] @@ -11,19 +11,6 @@ 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; @@ -38,29 +25,9 @@ impl ReadStorage for MutexFlashStorage { } } -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())) +impl embedded_storage::Storage for MutexFlashStorage { + fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> { + NorFlash::write(self, offset, bytes) } } @@ -68,10 +35,6 @@ 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 99bfc82..fdd4556 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,6 +23,7 @@ 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}; @@ -132,9 +133,10 @@ 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,11 +263,12 @@ 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> { @@ -392,18 +395,11 @@ 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 @@ -411,12 +407,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 { @@ -430,17 +426,20 @@ 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()); @@ -483,7 +482,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; @@ -512,6 +511,7 @@ impl<'a> BoardInteraction<'a> for V4<'a> { } } + async fn wait_for_can_measurements( as_async: &mut Twai<'_, Async>, moistures: &mut Moistures, @@ -538,12 +538,10 @@ 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 0fbe2ab..a686c6e 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::vec::Vec; use alloc::{format, vec}; +use alloc::vec::Vec; use chrono::{DateTime, Datelike, Timelike, Utc}; use chrono_tz::Tz::{self, UTC}; use core::sync::atomic::{AtomicBool, Ordering}; @@ -42,7 +42,6 @@ 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; @@ -502,17 +501,16 @@ 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 { @@ -531,10 +529,22 @@ 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"); } @@ -783,7 +793,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?; @@ -791,9 +801,10 @@ 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, @@ -973,7 +984,11 @@ 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(); @@ -1006,7 +1021,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 { @@ -1089,38 +1104,35 @@ 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(()), - 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(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(()) + } + + } } - }, + } Err(_) => { error!("Error reading serial line"); Ok(()) @@ -1176,35 +1188,13 @@ async fn get_version( let branch = env!("VERGEN_GIT_BRANCH").to_owned(); let hash = &env!("VERGEN_GIT_SHA")[0..8]; - 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) - } - }; - + let board = board.board_hal.get_esp(); VersionInfo { git_hash: branch + "@" + hash, build_time: env!("VERGEN_BUILD_TIMESTAMP").to_owned(), - current: format!("{:?}", current_partition), - state: format!("{:?}", current_state), + current: format!("{:?}", board.current), + slot0_state: format!("{:?}", board.slot0_state), + slot1_state: format!("{:?}", board.slot1_state), } } @@ -1213,5 +1203,6 @@ struct VersionInfo { git_hash: String, build_time: String, current: String, - state: String, + slot0_state: String, + slot1_state: String, } diff --git a/Software/MainBoard/rust/src/plant_state.rs b/Software/MainBoard/rust/src/plant_state.rs index 26f914b..0e92eee 100644 --- a/Software/MainBoard/rust/src/plant_state.rs +++ b/Software/MainBoard/rust/src/plant_state.rs @@ -16,6 +16,7 @@ pub enum MoistureSensorError { #[derive(Debug, PartialEq, Serialize)] pub enum MoistureSensorState { + Disabled, MoistureValue { raw_hz: f32, moisture_percent: f32 }, SensorError(MoistureSensorError), } @@ -116,11 +117,12 @@ 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, @@ -139,15 +141,17 @@ impl PlantState { } } } - }; // else { - // MoistureSensorState::Disabled - //}; - let sensor_b = { - //if board.board_hal.get_config().plants[plant_id].sensor_b { + }; // else { + // MoistureSensorState::Disabled + //}; + + 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, @@ -167,8 +171,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 107ee90..e801b35 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; +use alloc::string::{String, ToString}; use alloc::vec::Vec; use chrono_tz::Tz; use core::str::FromStr; @@ -43,6 +43,7 @@ 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, @@ -52,6 +53,7 @@ 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, @@ -126,7 +128,11 @@ 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 0a22122..c8195df 100644 --- a/Software/MainBoard/rust/src/webserver/ota.rs +++ b/Software/MainBoard/rust/src/webserver/ota.rs @@ -4,8 +4,7 @@ use crate::BOARD_ACCESS; use edge_http::io::server::Connection; use edge_http::Method; use edge_nal::io::{Read, Write}; -use esp_hal_ota::OtaError; -use log::{error, info}; +use log::info; pub(crate) async fn ota_operations( conn: &mut Connection<'_, T, { N }>, @@ -29,45 +28,27 @@ where Some(200) } Method::Post => { - 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 offset = 0_usize; let mut chunk = 0; loop { let buf = read_up_to_bytes_from_request(conn, Some(4096)).await?; if buf.is_empty() { - error!("Upload finished, but no enough data received."); - return Err(OtaError::WrongCRC)?; + info!("file request for ota finished"); + let mut board = BOARD_ACCESS.get().await.lock().await; + board.board_hal.get_esp().finalize_ota().await?; + break; } 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) - 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 - ); + info!("erasing and writing block 0x{offset:x}"); + board + .board_hal + .get_esp() + .write_ota(offset as u32, &buf) + .await?; } + 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 c92fa75..e49cac2 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}; +use crate::hal::{esp_set_time, Detection, DetectionRequest}; use crate::webserver::read_up_to_bytes_from_request; use crate::{do_secure_pump, BOARD_ACCESS}; use alloc::string::{String, ToString}; @@ -142,6 +142,9 @@ 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 80aeea9..876390b 100644 --- a/Software/MainBoard/rust/src_webpack/src/api.ts +++ b/Software/MainBoard/rust/src_webpack/src/api.ts @@ -163,7 +163,8 @@ export interface VersionInfo { git_hash: string, build_time: string, current: string, - state: string + slot0_state: string, + slot1_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 a9dc729..f3dcf9f 100644 --- a/Software/MainBoard/rust/src_webpack/src/main.ts +++ b/Software/MainBoard/rust/src_webpack/src/main.ts @@ -191,70 +191,36 @@ export class Controller { } uploadNewFirmware(file: File) { - 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; + 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; 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; - 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("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}`); } - } - return (crc ^ 0xFFFFFFFF) >>> 0; + }, 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); } async version(): Promise { @@ -298,14 +264,12 @@ 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 { @@ -326,6 +290,7 @@ export class Controller { method: "POST", body: pretty }).then( + _ => controller.progressview.removeProgress("write_rtc") ) } @@ -413,9 +378,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"); @@ -566,7 +531,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 dd791ed..c407932 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_state: HTMLDivElement; - + readonly firmware_state0: HTMLDivElement; + readonly firmware_state1: HTMLDivElement; constructor(controller: Controller) { (document.getElementById("firmwareview") as HTMLElement).innerHTML = require("./ota.html") @@ -18,7 +18,8 @@ export class OTAView { this.firmware_githash = document.getElementById("firmware_githash") as HTMLDivElement; this.firmware_partition = document.getElementById("firmware_partition") as HTMLDivElement; - this.firmware_state = document.getElementById("firmware_state") as HTMLDivElement; + this.firmware_state0 = document.getElementById("firmware_state0") as HTMLDivElement; + this.firmware_state1 = document.getElementById("firmware_state1") as HTMLDivElement; const file = document.getElementById("firmware_file") as HTMLInputElement; @@ -41,6 +42,7 @@ 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_state.innerText = versionInfo.state; + this.firmware_state0.innerText = versionInfo.slot0_state; + this.firmware_state1.innerText = versionInfo.slot1_state; } } \ No newline at end of file