fix ota abort/invalid switching
This commit is contained in:
12
rust/all.sh
Executable file
12
rust/all.sh
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
rm ./src/webserver/index.html.gz
|
||||||
|
rm ./src/webserver/bundle.js.gz
|
||||||
|
set -e
|
||||||
|
cd ./src_webpack/
|
||||||
|
npx webpack build
|
||||||
|
cp index.html.gz ../src/webserver/index.html.gz
|
||||||
|
cp bundle.js.gz ../src/webserver/bundle.js.gz
|
||||||
|
cd ../
|
||||||
|
|
||||||
|
cargo build --release
|
||||||
|
espflash save-image --bootloader bootloader.bin --partition-table partitions.csv --chip esp32c6 target/riscv32imac-unknown-none-elf/release/plant-ctrl2 image.bin
|
||||||
|
espflash flash --monitor --bootloader bootloader.bin --chip esp32c6 --baud 921600 --partition-table partitions.csv target/riscv32imac-unknown-none-elf/release/plant-ctrl2
|
@@ -7,5 +7,5 @@ cp index.html.gz ../src/webserver/index.html.gz
|
|||||||
cp bundle.js.gz ../src/webserver/bundle.js.gz
|
cp bundle.js.gz ../src/webserver/bundle.js.gz
|
||||||
cd ../
|
cd ../
|
||||||
|
|
||||||
cargo build
|
cargo build --release
|
||||||
espflash flash --monitor --bootloader bootloader.bin --chip esp32c6 --baud 921600 --partition-table partitions.csv target/riscv32imac-unknown-none-elf/release/plant-ctrl2
|
espflash flash --monitor --bootloader bootloader.bin --chip esp32c6 --baud 921600 --partition-table partitions.csv target/riscv32imac-unknown-none-elf/release/plant-ctrl2
|
||||||
|
@@ -65,6 +65,9 @@ pub enum FatError {
|
|||||||
CanBusError {
|
CanBusError {
|
||||||
error: EspTwaiError,
|
error: EspTwaiError,
|
||||||
},
|
},
|
||||||
|
SNTPError {
|
||||||
|
error: sntpc::Error,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type FatResult<T> = Result<T, FatError>;
|
pub type FatResult<T> = Result<T, FatError>;
|
||||||
@@ -96,6 +99,7 @@ impl fmt::Display for FatError {
|
|||||||
FatError::CanBusError { error } => {
|
FatError::CanBusError { error } => {
|
||||||
write!(f, "CanBusError {:?}", error)
|
write!(f, "CanBusError {:?}", error)
|
||||||
}
|
}
|
||||||
|
FatError::SNTPError { error } => write!(f, "SNTPError {:?}", error),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -299,10 +303,16 @@ impl From<nb::Error<EspTwaiError>> for FatError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<NorFlashErrorKind> for FatError{
|
impl From<NorFlashErrorKind> for FatError {
|
||||||
fn from(value: NorFlashErrorKind) -> Self {
|
fn from(value: NorFlashErrorKind) -> Self {
|
||||||
FatError::String {
|
FatError::String {
|
||||||
error: value.to_string()
|
error: value.to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<sntpc::Error> for FatError {
|
||||||
|
fn from(value: sntpc::Error) -> Self {
|
||||||
|
FatError::SNTPError { error: value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
use crate::bail;
|
use crate::bail;
|
||||||
use crate::config::{NetworkConfig, PlantControllerConfig};
|
use crate::config::{NetworkConfig, PlantControllerConfig};
|
||||||
use crate::hal::{get_next_slot, PLANT_COUNT, TIME_ACCESS};
|
use crate::hal::{get_current_slot_and_fix_ota_data, PLANT_COUNT, TIME_ACCESS};
|
||||||
use crate::log::{LogMessage, LOG_ACCESS};
|
use crate::log::{LogMessage, LOG_ACCESS};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -19,10 +19,10 @@ use embassy_net::{DhcpConfig, Ipv4Cidr, Runner, Stack, StackResources, StaticCon
|
|||||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||||
use embassy_sync::mutex::{Mutex, MutexGuard};
|
use embassy_sync::mutex::{Mutex, MutexGuard};
|
||||||
use embassy_sync::once_lock::OnceLock;
|
use embassy_sync::once_lock::OnceLock;
|
||||||
use embassy_time::{Duration, Timer};
|
use embassy_time::{Duration, Timer, WithTimeout};
|
||||||
use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash};
|
use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash};
|
||||||
use esp_bootloader_esp_idf::ota::OtaImageState::Valid;
|
use esp_bootloader_esp_idf::ota::OtaImageState::Valid;
|
||||||
use esp_bootloader_esp_idf::ota::{Ota, OtaImageState};
|
use esp_bootloader_esp_idf::ota::{Ota, OtaImageState, Slot};
|
||||||
use esp_bootloader_esp_idf::partitions::FlashRegion;
|
use esp_bootloader_esp_idf::partitions::FlashRegion;
|
||||||
use esp_hal::gpio::{Input, RtcPinWithResistors};
|
use esp_hal::gpio::{Input, RtcPinWithResistors};
|
||||||
use esp_hal::rng::Rng;
|
use esp_hal::rng::Rng;
|
||||||
@@ -128,7 +128,10 @@ pub struct Esp<'a> {
|
|||||||
pub wake_gpio1: esp_hal::peripherals::GPIO1<'static>,
|
pub wake_gpio1: esp_hal::peripherals::GPIO1<'static>,
|
||||||
|
|
||||||
pub ota: Ota<'static, FlashStorage>,
|
pub ota: Ota<'static, FlashStorage>,
|
||||||
pub ota_next: &'static mut FlashRegion<'static, FlashStorage>,
|
pub ota_target: &'static mut FlashRegion<'static, FlashStorage>,
|
||||||
|
pub current: Slot,
|
||||||
|
pub slot0_state: OtaImageState,
|
||||||
|
pub slot1_state: OtaImageState,
|
||||||
}
|
}
|
||||||
|
|
||||||
// SAFETY: On this target we never move Esp across OS threads; the firmware runs single-core
|
// SAFETY: On this target we never move Esp across OS threads; the firmware runs single-core
|
||||||
@@ -213,43 +216,15 @@ impl Esp<'_> {
|
|||||||
Ok((buf, read))
|
Ok((buf, read))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_current_ota_slot(&mut self) -> String {
|
|
||||||
match get_next_slot(&mut self.ota) {
|
|
||||||
Ok(slot) => {
|
|
||||||
format!("{:?}", slot.next())
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
format!("{:?}", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_ota_state(&mut self) -> String {
|
|
||||||
match self.ota.current_ota_state() {
|
|
||||||
Ok(state) => {
|
|
||||||
format!("{:?}", state)
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
format!("{:?}", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn write_ota(&mut self, offset: u32, buf: &[u8]) -> Result<(), FatError> {
|
pub(crate) async fn write_ota(&mut self, offset: u32, buf: &[u8]) -> Result<(), FatError> {
|
||||||
if self.ota.current_ota_state() == Ok(OtaImageState::Invalid) {
|
let _ = check_erase(self.ota_target, offset, offset + 4096);
|
||||||
bail!("Invalid OTA state, refusing ota write")
|
self.ota_target.erase(offset, offset + 4096)?;
|
||||||
}
|
|
||||||
if self.ota.current_ota_state() == Ok(OtaImageState::Undefined) {
|
|
||||||
bail!("Invalid OTA state, refusing ota write")
|
|
||||||
}
|
|
||||||
let _ = check_erase(self.ota_next, offset, offset + 4096);
|
|
||||||
self.ota_next.erase(offset, offset + 4096)?;
|
|
||||||
|
|
||||||
let mut temp = vec![0; buf.len()];
|
let mut temp = vec![0; buf.len()];
|
||||||
let read_back = temp.as_mut_slice();
|
let read_back = temp.as_mut_slice();
|
||||||
//change to nor flash, align writes!
|
//change to nor flash, align writes!
|
||||||
self.ota_next.write(offset, buf)?;
|
self.ota_target.write(offset, buf)?;
|
||||||
self.ota_next.read(offset, read_back)?;
|
self.ota_target.read(offset, read_back)?;
|
||||||
if buf != read_back {
|
if buf != read_back {
|
||||||
info!("Expected {:?} but got {:?}", buf, read_back);
|
info!("Expected {:?} but got {:?}", buf, read_back);
|
||||||
bail!(
|
bail!(
|
||||||
@@ -261,23 +236,16 @@ impl Esp<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn finalize_ota(&mut self) -> Result<(), FatError> {
|
pub(crate) async fn finalize_ota(&mut self) -> Result<(), FatError> {
|
||||||
if self.ota.current_ota_state() == Ok(OtaImageState::Invalid) {
|
let current = self.ota.current_slot()?;
|
||||||
bail!("Invalid OTA state, refusing ota write")
|
if self.ota.current_ota_state()? != OtaImageState::Valid {
|
||||||
}
|
info!(
|
||||||
if self.ota.current_ota_state() == Ok(OtaImageState::Undefined) {
|
"Validating current slot {:?} as it was able to ota",
|
||||||
bail!("Invalid OTA state, refusing ota write")
|
current
|
||||||
}
|
);
|
||||||
|
|
||||||
let current_state = self.ota.current_ota_state()?;
|
|
||||||
info!("current state {:?}", current_state);
|
|
||||||
let next_slot = get_next_slot(&mut self.ota)?;
|
|
||||||
info!("current slot {:?}", next_slot.next());
|
|
||||||
if current_state == OtaImageState::PendingVerify {
|
|
||||||
info!("verifying ota image from pending");
|
|
||||||
self.ota.set_current_ota_state(Valid)?;
|
self.ota.set_current_ota_state(Valid)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.ota.set_current_slot(next_slot)?;
|
self.ota.set_current_slot(current.next())?;
|
||||||
info!("switched slot");
|
info!("switched slot");
|
||||||
self.ota.set_current_ota_state(OtaImageState::New)?;
|
self.ota.set_current_ota_state(OtaImageState::New)?;
|
||||||
info!("switched state for new partition");
|
info!("switched state for new partition");
|
||||||
@@ -327,16 +295,19 @@ impl Esp<'_> {
|
|||||||
let mut counter = 0;
|
let mut counter = 0;
|
||||||
loop {
|
loop {
|
||||||
let addr: IpAddr = ntp_addrs[0].into();
|
let addr: IpAddr = ntp_addrs[0].into();
|
||||||
let result = get_time(SocketAddr::from((addr, 123)), &socket, context).await;
|
let timeout = get_time(SocketAddr::from((addr, 123)), &socket, context)
|
||||||
|
.with_timeout(Duration::from_millis((_max_wait_ms / 10) as u64))
|
||||||
|
.await;
|
||||||
|
|
||||||
match result {
|
match timeout {
|
||||||
Ok(time) => {
|
Ok(result) => {
|
||||||
|
let time = result?;
|
||||||
info!("Time: {:?}", time);
|
info!("Time: {:?}", time);
|
||||||
return DateTime::from_timestamp(time.seconds as i64, 0)
|
return DateTime::from_timestamp(time.seconds as i64, 0)
|
||||||
.context("Could not convert Sntp result");
|
.context("Could not convert Sntp result");
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(err) => {
|
||||||
warn!("Error: {:?}", e);
|
warn!("sntp timeout, retry: {:?}", err);
|
||||||
counter += 1;
|
counter += 1;
|
||||||
if counter > 10 {
|
if counter > 10 {
|
||||||
bail!("Failed to get time from NTP server");
|
bail!("Failed to get time from NTP server");
|
||||||
|
@@ -9,7 +9,6 @@ mod v3_shift_register;
|
|||||||
mod v4_hal;
|
mod v4_hal;
|
||||||
mod v4_sensor;
|
mod v4_sensor;
|
||||||
mod water;
|
mod water;
|
||||||
|
|
||||||
use crate::alloc::string::ToString;
|
use crate::alloc::string::ToString;
|
||||||
use crate::hal::rtc::{DS3231Module, RTCModuleInteraction};
|
use crate::hal::rtc::{DS3231Module, RTCModuleInteraction};
|
||||||
use esp_hal::peripherals::Peripherals;
|
use esp_hal::peripherals::Peripherals;
|
||||||
@@ -44,6 +43,7 @@ use esp_hal::peripherals::GPIO8;
|
|||||||
use esp_hal::peripherals::TWAI0;
|
use esp_hal::peripherals::TWAI0;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
bail,
|
||||||
config::{BatteryBoardVersion, BoardVersion, PlantControllerConfig},
|
config::{BatteryBoardVersion, BoardVersion, PlantControllerConfig},
|
||||||
hal::{
|
hal::{
|
||||||
battery::{BatteryInteraction, NoBatteryMonitor},
|
battery::{BatteryInteraction, NoBatteryMonitor},
|
||||||
@@ -83,9 +83,11 @@ use crate::hal::water::TankSensor;
|
|||||||
use crate::log::LOG_ACCESS;
|
use crate::log::LOG_ACCESS;
|
||||||
use embassy_sync::mutex::Mutex;
|
use embassy_sync::mutex::Mutex;
|
||||||
use embassy_sync::once_lock::OnceLock;
|
use embassy_sync::once_lock::OnceLock;
|
||||||
|
use embedded_storage::nor_flash::ReadNorFlash;
|
||||||
use esp_alloc as _;
|
use esp_alloc as _;
|
||||||
use esp_backtrace as _;
|
use esp_backtrace as _;
|
||||||
use esp_bootloader_esp_idf::ota::{Ota, OtaImageState, Slot};
|
use esp_bootloader_esp_idf::ota::{Ota, OtaImageState};
|
||||||
|
use esp_bootloader_esp_idf::ota::{Slot as ota_slot, Slot};
|
||||||
use esp_hal::delay::Delay;
|
use esp_hal::delay::Delay;
|
||||||
use esp_hal::i2c::master::{BusTimeout, Config, I2c};
|
use esp_hal::i2c::master::{BusTimeout, Config, I2c};
|
||||||
use esp_hal::pcnt::unit::Unit;
|
use esp_hal::pcnt::unit::Unit;
|
||||||
@@ -101,12 +103,15 @@ use esp_wifi::{init, EspWifiController};
|
|||||||
use littlefs2::fs::{Allocation, Filesystem as lfs2Filesystem};
|
use littlefs2::fs::{Allocation, Filesystem as lfs2Filesystem};
|
||||||
use littlefs2::object_safe::DynStorage;
|
use littlefs2::object_safe::DynStorage;
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
|
use portable_atomic::AtomicBool;
|
||||||
|
|
||||||
pub static TIME_ACCESS: OnceLock<Mutex<CriticalSectionRawMutex, Rtc>> = OnceLock::new();
|
pub static TIME_ACCESS: OnceLock<Mutex<CriticalSectionRawMutex, Rtc>> = OnceLock::new();
|
||||||
|
|
||||||
//Only support for 8 right now!
|
//Only support for 8 right now!
|
||||||
pub const PLANT_COUNT: usize = 8;
|
pub const PLANT_COUNT: usize = 8;
|
||||||
|
|
||||||
|
pub static PROGRESS_ACTIVE: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
const TANK_MULTI_SAMPLE: usize = 11;
|
const TANK_MULTI_SAMPLE: usize = 11;
|
||||||
pub static I2C_DRIVER: OnceLock<
|
pub static I2C_DRIVER: OnceLock<
|
||||||
embassy_sync::blocking_mutex::Mutex<CriticalSectionRawMutex, RefCell<I2c<Blocking>>>,
|
embassy_sync::blocking_mutex::Mutex<CriticalSectionRawMutex, RefCell<I2c<Blocking>>>,
|
||||||
@@ -148,6 +153,9 @@ pub trait BoardInteraction<'a> {
|
|||||||
async fn get_mptt_current(&mut self) -> Result<Current, FatError>;
|
async fn get_mptt_current(&mut self) -> Result<Current, FatError>;
|
||||||
|
|
||||||
async fn progress(&mut self, counter: u32) {
|
async fn progress(&mut self, counter: u32) {
|
||||||
|
// Indicate progress is active to suppress default wait_infinity blinking
|
||||||
|
crate::hal::PROGRESS_ACTIVE.store(true, core::sync::atomic::Ordering::Relaxed);
|
||||||
|
|
||||||
let current = counter % PLANT_COUNT as u32;
|
let current = counter % PLANT_COUNT as u32;
|
||||||
for led in 0..PLANT_COUNT {
|
for led in 0..PLANT_COUNT {
|
||||||
if let Err(err) = self.fault(led, current == led as u32).await {
|
if let Err(err) = self.fault(led, current == led as u32).await {
|
||||||
@@ -165,6 +173,9 @@ pub trait BoardInteraction<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = self.general_fault(false).await;
|
let _ = self.general_fault(false).await;
|
||||||
|
|
||||||
|
// Reset progress active flag so wait_infinity can resume blinking
|
||||||
|
crate::hal::PROGRESS_ACTIVE.store(false, core::sync::atomic::Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,10 +314,6 @@ impl PlantHal {
|
|||||||
let pt =
|
let pt =
|
||||||
esp_bootloader_esp_idf::partitions::read_partition_table(storage_ota, tablebuffer)?;
|
esp_bootloader_esp_idf::partitions::read_partition_table(storage_ota, tablebuffer)?;
|
||||||
|
|
||||||
// List all partitions - this is just FYI
|
|
||||||
for i in 0..pt.len() {
|
|
||||||
info!("{:?}", pt.get_partition(i));
|
|
||||||
}
|
|
||||||
let ota_data = mk_static!(
|
let ota_data = mk_static!(
|
||||||
PartitionEntry,
|
PartitionEntry,
|
||||||
pt.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data(
|
pt.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data(
|
||||||
@@ -320,13 +327,18 @@ impl PlantHal {
|
|||||||
ota_data.as_embedded_storage(storage_ota)
|
ota_data.as_embedded_storage(storage_ota)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let state_0 = ota_state(ota_slot::Slot0, ota_data);
|
||||||
|
let state_1 = ota_state(ota_slot::Slot1, ota_data);
|
||||||
let mut ota = Ota::new(ota_data)?;
|
let mut ota = Ota::new(ota_data)?;
|
||||||
|
let running = get_current_slot_and_fix_ota_data(&mut ota, state_0, state_1)?;
|
||||||
|
let target = running.next();
|
||||||
|
|
||||||
let state = ota.current_ota_state().unwrap_or_default();
|
info!("Currently running OTA slot: {:?}", running);
|
||||||
info!("Current OTA state: {:?}", state);
|
info!("Slot0 state: {:?}", state_0);
|
||||||
let next_slot = get_next_slot(&mut ota)?;
|
info!("Slot1 state: {:?}", state_1);
|
||||||
info!("Next OTA slot: {:?}", next_slot);
|
|
||||||
let ota_next = match next_slot {
|
//obtain current_state and next_state here!
|
||||||
|
let ota_target = match target {
|
||||||
Slot::None => {
|
Slot::None => {
|
||||||
panic!("No OTA slot active?");
|
panic!("No OTA slot active?");
|
||||||
}
|
}
|
||||||
@@ -342,11 +354,11 @@ impl PlantHal {
|
|||||||
.context("Partition table invalid no ota1")?,
|
.context("Partition table invalid no ota1")?,
|
||||||
};
|
};
|
||||||
|
|
||||||
let ota_next = mk_static!(PartitionEntry, ota_next);
|
let ota_target = mk_static!(PartitionEntry, ota_target);
|
||||||
let storage_ota = mk_static!(FlashStorage, FlashStorage::new());
|
let storage_ota = mk_static!(FlashStorage, FlashStorage::new());
|
||||||
let ota_next = mk_static!(
|
let ota_target = mk_static!(
|
||||||
FlashRegion<FlashStorage>,
|
FlashRegion<FlashStorage>,
|
||||||
ota_next.as_embedded_storage(storage_ota)
|
ota_target.as_embedded_storage(storage_ota)
|
||||||
);
|
);
|
||||||
|
|
||||||
let data_partition = pt
|
let data_partition = pt
|
||||||
@@ -391,7 +403,10 @@ impl PlantHal {
|
|||||||
boot_button,
|
boot_button,
|
||||||
wake_gpio1,
|
wake_gpio1,
|
||||||
ota,
|
ota,
|
||||||
ota_next,
|
ota_target,
|
||||||
|
current: running,
|
||||||
|
slot0_state: state_0,
|
||||||
|
slot1_state: state_1,
|
||||||
};
|
};
|
||||||
|
|
||||||
//init,reset rtc memory depending on cause
|
//init,reset rtc memory depending on cause
|
||||||
@@ -573,29 +588,75 @@ impl PlantHal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_next_slot(ota: &mut Ota<FlashStorage>) -> Result<Slot, FatError> {
|
fn ota_state(slot: ota_slot, ota_data: &mut FlashRegion<FlashStorage>) -> OtaImageState {
|
||||||
let next_slot = {
|
// Read and log OTA states for both slots before constructing Ota
|
||||||
let state = ota.current_ota_state().unwrap_or_default();
|
// Each OTA select entry is 32 bytes: [seq:4][label:20][state:4][crc:4]
|
||||||
let current = ota.current_slot()?;
|
// Offsets within the OTA data partition: slot0 @ 0x0000, slot1 @ 0x1000
|
||||||
match state {
|
if slot == ota_slot::None {
|
||||||
OtaImageState::New => current.next(),
|
return OtaImageState::Undefined;
|
||||||
OtaImageState::PendingVerify => current.next(),
|
}
|
||||||
OtaImageState::Valid => current.next(),
|
let mut slot_buf = [0u8; 32];
|
||||||
|
if slot == ota_slot::Slot0 {
|
||||||
|
let _ = ota_data.read(0x0000, &mut slot_buf);
|
||||||
|
} else {
|
||||||
|
let _ = ota_data.read(0x1000, &mut slot_buf);
|
||||||
|
}
|
||||||
|
let raw_state = u32::from_le_bytes(slot_buf[24..28].try_into().unwrap_or([0xff; 4]));
|
||||||
|
let state0 = OtaImageState::try_from(raw_state).unwrap_or(OtaImageState::Undefined);
|
||||||
|
state0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_current_slot_and_fix_ota_data(
|
||||||
|
ota: &mut Ota<FlashStorage>,
|
||||||
|
state0: OtaImageState,
|
||||||
|
state1: OtaImageState,
|
||||||
|
) -> Result<ota_slot, FatError> {
|
||||||
|
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_slot()?;
|
||||||
|
if swap {
|
||||||
|
let other = match current {
|
||||||
|
ota_slot::Slot0 => state1,
|
||||||
|
ota_slot::Slot1 => state0,
|
||||||
|
_ => OtaImageState::Invalid,
|
||||||
|
};
|
||||||
|
|
||||||
|
match other {
|
||||||
OtaImageState::Invalid => {
|
OtaImageState::Invalid => {
|
||||||
//we actually booted other slot, than partition table assumes
|
bail!(
|
||||||
current
|
"cannot recover slot, as both slots in invalid state {:?} {:?} {:?}",
|
||||||
|
current,
|
||||||
|
state0,
|
||||||
|
state1
|
||||||
|
);
|
||||||
}
|
}
|
||||||
OtaImageState::Aborted => {
|
OtaImageState::Aborted => {
|
||||||
//we actually booted other slot, than partition table assumes
|
bail!(
|
||||||
current
|
"cannot recover slot, as both slots in invalid state {:?} {:?} {:?}",
|
||||||
}
|
current,
|
||||||
OtaImageState::Undefined => {
|
state0,
|
||||||
//missing bootloader?
|
state1
|
||||||
current.next()
|
);
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
|
info!(
|
||||||
|
"Current slot has state {:?} other state has {:?} swapping",
|
||||||
|
state, other
|
||||||
|
);
|
||||||
|
ota.set_current_slot(current.next())?;
|
||||||
|
//we actually booted other slot, than partition table assumes
|
||||||
|
return Ok(ota.current_slot()?);
|
||||||
};
|
};
|
||||||
Ok(next_slot)
|
Ok(current)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn esp_time() -> DateTime<Utc> {
|
pub async fn esp_time() -> DateTime<Utc> {
|
||||||
|
@@ -17,6 +17,7 @@ use crate::config::{NetworkConfig, PlantConfig};
|
|||||||
use crate::fat_error::FatResult;
|
use crate::fat_error::FatResult;
|
||||||
use crate::hal::esp::MQTT_STAY_ALIVE;
|
use crate::hal::esp::MQTT_STAY_ALIVE;
|
||||||
use crate::hal::{esp_time, TIME_ACCESS};
|
use crate::hal::{esp_time, TIME_ACCESS};
|
||||||
|
use crate::hal::PROGRESS_ACTIVE;
|
||||||
use crate::log::{log, LOG_ACCESS};
|
use crate::log::{log, LOG_ACCESS};
|
||||||
use crate::tank::{determine_tank_state, TankError, TankState, WATER_FROZEN_THRESH};
|
use crate::tank::{determine_tank_state, TankError, TankState, WATER_FROZEN_THRESH};
|
||||||
use crate::webserver::http_server;
|
use crate::webserver::http_server;
|
||||||
@@ -822,18 +823,9 @@ async fn publish_firmware_info(
|
|||||||
let esp = board.board_hal.get_esp();
|
let esp = board.board_hal.get_esp();
|
||||||
let _ = esp.mqtt_publish("/firmware/address", ip_address).await;
|
let _ = esp.mqtt_publish("/firmware/address", ip_address).await;
|
||||||
let _ = esp
|
let _ = esp
|
||||||
.mqtt_publish("/firmware/githash", &version.git_hash)
|
.mqtt_publish("/firmware/state", format!("{:?}", &version).as_str())
|
||||||
.await;
|
|
||||||
let _ = esp
|
|
||||||
.mqtt_publish("/firmware/buildtime", &version.build_time)
|
|
||||||
.await;
|
.await;
|
||||||
let _ = esp.mqtt_publish("/firmware/last_online", timezone_time);
|
let _ = esp.mqtt_publish("/firmware/last_online", timezone_time);
|
||||||
let state = esp.get_ota_state();
|
|
||||||
let _ = esp.mqtt_publish("/firmware/ota_state", &state).await;
|
|
||||||
let slot = esp.get_current_ota_slot();
|
|
||||||
let _ = esp
|
|
||||||
.mqtt_publish("/firmware/ota_slot", &format!("slot{slot}"))
|
|
||||||
.await;
|
|
||||||
let _ = esp.mqtt_publish("/state", "online").await;
|
let _ = esp.mqtt_publish("/state", "online").await;
|
||||||
}
|
}
|
||||||
macro_rules! mk_static {
|
macro_rules! mk_static {
|
||||||
@@ -995,41 +987,48 @@ async fn wait_infinity(
|
|||||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||||
update_charge_indicator(&mut board).await;
|
update_charge_indicator(&mut board).await;
|
||||||
|
|
||||||
match wait_type {
|
// Skip default blink code when a progress display is active
|
||||||
WaitType::MissingConfig => {
|
if !PROGRESS_ACTIVE.load(Ordering::Relaxed) {
|
||||||
// Keep existing behavior: circular filling pattern
|
match wait_type {
|
||||||
led_count %= 8;
|
WaitType::MissingConfig => {
|
||||||
led_count += 1;
|
// Keep existing behavior: circular filling pattern
|
||||||
for i in 0..8 {
|
led_count %= 8;
|
||||||
let _ = board.board_hal.fault(i, i < led_count).await;
|
led_count += 1;
|
||||||
}
|
for i in 0..8 {
|
||||||
}
|
let _ = board.board_hal.fault(i, i < led_count).await;
|
||||||
WaitType::ConfigButton => {
|
}
|
||||||
// Alternating pattern: 1010 1010 -> 0101 0101
|
}
|
||||||
pattern_step = (pattern_step + 1) % 2;
|
WaitType::ConfigButton => {
|
||||||
for i in 0..8 {
|
// Alternating pattern: 1010 1010 -> 0101 0101
|
||||||
let _ = board.board_hal.fault(i, (i + pattern_step) % 2 == 0).await;
|
pattern_step = (pattern_step + 1) % 2;
|
||||||
}
|
for i in 0..8 {
|
||||||
}
|
let _ = board.board_hal.fault(i, (i + pattern_step) % 2 == 0).await;
|
||||||
WaitType::MqttConfig => {
|
}
|
||||||
// Moving dot pattern
|
}
|
||||||
pattern_step = (pattern_step + 1) % 8;
|
WaitType::MqttConfig => {
|
||||||
for i in 0..8 {
|
// Moving dot pattern
|
||||||
let _ = board.board_hal.fault(i, i == pattern_step).await;
|
pattern_step = (pattern_step + 1) % 8;
|
||||||
|
for i in 0..8 {
|
||||||
|
let _ = board.board_hal.fault(i, i == pattern_step).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
board.board_hal.general_fault(true).await;
|
||||||
}
|
}
|
||||||
board.board_hal.general_fault(true).await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer::after_millis(delay).await;
|
Timer::after_millis(delay).await;
|
||||||
{
|
{
|
||||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||||
board.board_hal.general_fault(false).await;
|
|
||||||
|
|
||||||
// Clear all LEDs
|
// Skip clearing LEDs when progress is active to avoid interrupting the progress display
|
||||||
for i in 0..8 {
|
if !PROGRESS_ACTIVE.load(Ordering::Relaxed) {
|
||||||
let _ = board.board_hal.fault(i, false).await;
|
board.board_hal.general_fault(false).await;
|
||||||
|
|
||||||
|
// Clear all LEDs
|
||||||
|
for i in 0..8 {
|
||||||
|
let _ = board.board_hal.fault(i, false).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1096,14 +1095,12 @@ async fn get_version(
|
|||||||
let hash = &env!("VERGEN_GIT_SHA")[0..8];
|
let hash = &env!("VERGEN_GIT_SHA")[0..8];
|
||||||
|
|
||||||
let board = board.board_hal.get_esp();
|
let board = board.board_hal.get_esp();
|
||||||
|
|
||||||
let ota_slot = board.get_current_ota_slot();
|
|
||||||
let ota_state = board.get_ota_state();
|
|
||||||
VersionInfo {
|
VersionInfo {
|
||||||
git_hash: branch + "@" + hash,
|
git_hash: branch + "@" + hash,
|
||||||
build_time: env!("VERGEN_BUILD_TIMESTAMP").to_owned(),
|
build_time: env!("VERGEN_BUILD_TIMESTAMP").to_owned(),
|
||||||
partition: ota_slot,
|
current: format!("{:?}", board.current),
|
||||||
ota_state,
|
slot0_state: format!("{:?}", board.slot0_state),
|
||||||
|
slot1_state: format!("{:?}", board.slot1_state),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1111,6 +1108,7 @@ async fn get_version(
|
|||||||
struct VersionInfo {
|
struct VersionInfo {
|
||||||
git_hash: String,
|
git_hash: String,
|
||||||
build_time: String,
|
build_time: String,
|
||||||
partition: String,
|
current: String,
|
||||||
ota_state: String,
|
slot0_state: String,
|
||||||
|
slot1_state: String,
|
||||||
}
|
}
|
||||||
|
@@ -240,34 +240,6 @@ pub async fn http_server(reboot_now: Arc<AtomicBool>, stack: Stack<'static>) {
|
|||||||
info!("Webserver started and waiting for connections");
|
info!("Webserver started and waiting for connections");
|
||||||
|
|
||||||
//TODO https if mbed_esp lands
|
//TODO https if mbed_esp lands
|
||||||
|
|
||||||
// server
|
|
||||||
// .fn_handler("/ota", Method::Post, |request| {
|
|
||||||
// handle_error_to500(request, ota)
|
|
||||||
// })
|
|
||||||
// .unwrap();
|
|
||||||
// server
|
|
||||||
// .fn_handler("/ota", Method::Options, |request| {
|
|
||||||
// cors_response(request, 200, "")
|
|
||||||
// })
|
|
||||||
// .unwrap();
|
|
||||||
// let reboot_now_for_reboot = reboot_now.clone();
|
|
||||||
// server
|
|
||||||
// .fn_handler("/reboot", Method::Post, move |_| {
|
|
||||||
// BOARD_ACCESS
|
|
||||||
// .lock()
|
|
||||||
// .unwrap()
|
|
||||||
// .board_hal
|
|
||||||
// .get_esp()
|
|
||||||
// .set_restart_to_conf(true);
|
|
||||||
// reboot_now_for_reboot.store(true, std::sync::atomic::Ordering::Relaxed);
|
|
||||||
// anyhow::Ok(())
|
|
||||||
// })
|
|
||||||
// .unwrap();
|
|
||||||
//
|
|
||||||
// unsafe { vTaskDelay(1) };
|
|
||||||
//
|
|
||||||
// server
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_json<'a, T, const N: usize>(
|
async fn handle_json<'a, T, const N: usize>(
|
||||||
|
@@ -30,9 +30,6 @@ where
|
|||||||
Method::Post => {
|
Method::Post => {
|
||||||
let mut offset = 0_usize;
|
let mut offset = 0_usize;
|
||||||
let mut chunk = 0;
|
let mut chunk = 0;
|
||||||
|
|
||||||
// Erase only a single 4K block right before writing into it.
|
|
||||||
// The first block will be erased when offset == 0 below.
|
|
||||||
loop {
|
loop {
|
||||||
let buf = read_up_to_bytes_from_request(conn, Some(4096)).await?;
|
let buf = read_up_to_bytes_from_request(conn, Some(4096)).await?;
|
||||||
if buf.len() == 0 {
|
if buf.len() == 0 {
|
||||||
|
@@ -157,9 +157,9 @@ export interface Moistures {
|
|||||||
export interface VersionInfo {
|
export interface VersionInfo {
|
||||||
git_hash: string,
|
git_hash: string,
|
||||||
build_time: string,
|
build_time: string,
|
||||||
partition: string,
|
current: string,
|
||||||
ota_state: string
|
slot0_state: string,
|
||||||
|
slot1_state: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BatteryState {
|
export interface BatteryState {
|
||||||
|
@@ -1,18 +1,21 @@
|
|||||||
<style>
|
<style>
|
||||||
.otakey{
|
.otakey {
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
.otavalue{
|
|
||||||
flex-grow: 1;
|
.otavalue {
|
||||||
}
|
flex-grow: 1;
|
||||||
.otaform {
|
}
|
||||||
min-width: 100px;
|
|
||||||
flex-grow: 1;
|
.otaform {
|
||||||
}
|
min-width: 100px;
|
||||||
.otachooser {
|
flex-grow: 1;
|
||||||
min-width: 100px;
|
}
|
||||||
width: 100%;
|
|
||||||
}
|
.otachooser {
|
||||||
|
min-width: 100px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="flexcontainer">
|
<div class="flexcontainer">
|
||||||
<div class="subtitle">
|
<div class="subtitle">
|
||||||
@@ -28,12 +31,16 @@
|
|||||||
<span class="otavalue" id="firmware_githash"></span>
|
<span class="otavalue" id="firmware_githash"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flexcontainer">
|
<div class="flexcontainer">
|
||||||
<span class="otakey">Partition:</span>
|
<span class="otakey">Partition:</span>
|
||||||
<span class="otavalue" id="firmware_partition"></span>
|
<span class="otavalue" id="firmware_partition"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flexcontainer">
|
<div class="flexcontainer">
|
||||||
<span class="otakey">Status:</span>
|
<span class="otakey">State0:</span>
|
||||||
<span class="otavalue" id="firmware_state"></span>
|
<span class="otavalue" id="firmware_state0"></span>
|
||||||
|
</div>
|
||||||
|
<div class="flexcontainer">
|
||||||
|
<span class="otakey">State1:</span>
|
||||||
|
<span class="otavalue" id="firmware_state1"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flexcontainer">
|
<div class="flexcontainer">
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Controller } from "./main";
|
import {Controller} from "./main";
|
||||||
import {VersionInfo} from "./api";
|
import {VersionInfo} from "./api";
|
||||||
|
|
||||||
export class OTAView {
|
export class OTAView {
|
||||||
@@ -6,19 +6,22 @@ export class OTAView {
|
|||||||
readonly firmware_buildtime: HTMLDivElement;
|
readonly firmware_buildtime: HTMLDivElement;
|
||||||
readonly firmware_githash: HTMLDivElement;
|
readonly firmware_githash: HTMLDivElement;
|
||||||
readonly firmware_partition: HTMLDivElement;
|
readonly firmware_partition: HTMLDivElement;
|
||||||
readonly firmware_state: HTMLDivElement;
|
readonly firmware_state0: HTMLDivElement;
|
||||||
|
readonly firmware_state1: HTMLDivElement;
|
||||||
|
|
||||||
constructor(controller: Controller) {
|
constructor(controller: Controller) {
|
||||||
(document.getElementById("firmwareview") as HTMLElement).innerHTML = require("./ota.html")
|
(document.getElementById("firmwareview") as HTMLElement).innerHTML = require("./ota.html")
|
||||||
|
|
||||||
let test = document.getElementById("test") as HTMLButtonElement;
|
let test = document.getElementById("test") as HTMLButtonElement;
|
||||||
|
|
||||||
this.firmware_buildtime = document.getElementById("firmware_buildtime") as HTMLDivElement;
|
this.firmware_buildtime = document.getElementById("firmware_buildtime") as HTMLDivElement;
|
||||||
this.firmware_githash = document.getElementById("firmware_githash") as HTMLDivElement;
|
this.firmware_githash = document.getElementById("firmware_githash") as HTMLDivElement;
|
||||||
this.firmware_partition = document.getElementById("firmware_partition") 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;
|
const file = document.getElementById("firmware_file") as HTMLInputElement;
|
||||||
this.file1Upload = file
|
this.file1Upload = file
|
||||||
this.file1Upload.onchange = () => {
|
this.file1Upload.onchange = () => {
|
||||||
@@ -38,7 +41,8 @@ export class OTAView {
|
|||||||
setVersion(versionInfo: VersionInfo) {
|
setVersion(versionInfo: VersionInfo) {
|
||||||
this.firmware_buildtime.innerText = versionInfo.build_time;
|
this.firmware_buildtime.innerText = versionInfo.build_time;
|
||||||
this.firmware_githash.innerText = versionInfo.git_hash;
|
this.firmware_githash.innerText = versionInfo.git_hash;
|
||||||
this.firmware_partition.innerText = versionInfo.partition;
|
this.firmware_partition.innerText = versionInfo.current;
|
||||||
this.firmware_state.innerText = versionInfo.ota_state;
|
this.firmware_state0.innerText = versionInfo.slot0_state;
|
||||||
|
this.firmware_state1.innerText = versionInfo.slot1_state;
|
||||||
}
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user