Fix ota, use MMU to determine running partition, use RMW wrapper for ota_data partition (littelfs handles this internally, so it was no problem prior)

This commit is contained in:
2026-03-18 01:22:33 +01:00
parent 66e1fe63e0
commit 39e4e733f3
11 changed files with 220 additions and 249 deletions

View File

@@ -21,11 +21,10 @@ 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 embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash, RmwNorFlashStorage};
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 +33,7 @@ use esp_hal::rtc_cntl::{
};
use esp_hal::system::software_reset;
use esp_hal::uart::Uart;
use esp_hal::Blocking;
use esp_println::println;
use esp_radio::wifi::{
AccessPointConfig, AccessPointInfo, AuthMethod, ClientConfig, ModeConfig, ScanConfig,
@@ -130,7 +130,7 @@ pub struct Esp<'a> {
pub wake_gpio1: esp_hal::peripherals::GPIO1<'static>,
pub uart0: Uart<'a, Blocking>,
pub ota: Ota<'static, MutexFlashStorage>,
pub ota: Ota<'static, RmwNorFlashStorage<'static, &'static mut MutexFlashStorage>>,
pub ota_target: &'static mut FlashRegion<'static, MutexFlashStorage>,
pub current: AppPartitionSubType,
pub slot0_state: OtaImageState,
@@ -155,7 +155,6 @@ macro_rules! mk_static {
}
impl Esp<'_> {
pub(crate) async fn read_serial_line(&mut self) -> FatResult<Option<alloc::string::String>> {
let mut buf = [0u8; 1];
let mut line = String::new();
@@ -171,7 +170,7 @@ impl Esp<'_> {
}
line.push(c);
}
Err(error ) => {
Err(error) => {
if line.is_empty() {
return Ok(None);
} else {
@@ -252,6 +251,7 @@ impl Esp<'_> {
pub(crate) async fn write_ota(&mut self, offset: u32, buf: &[u8]) -> Result<(), FatError> {
let _ = check_erase(self.ota_target, offset, offset + 4096);
info!("erasing and writing block 0x{offset:x}");
self.ota_target.erase(offset, offset + 4096)?;
let mut temp = vec![0; buf.len()];
@@ -288,8 +288,6 @@ impl Esp<'_> {
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(())
}
@@ -597,10 +595,13 @@ impl Esp<'_> {
// 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 {
info!("Marking OTA image as valid");
self.ota
.set_current_ota_state(Valid)
.expect("Could not set image to valid");
}
} else {
info!("No OTA image to mark as valid");
}
if duration_in_ms == 0 {

View File

@@ -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,
@@ -66,19 +66,21 @@ 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,
AppPartitionSubType, DataPartitionSubType, FlashRegion, PartitionEntry, PartitionTable,
PartitionType,
};
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::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::nor_flash::RmwNorFlashStorage;
use embedded_storage::ReadStorage;
use esp_alloc as _;
use esp_backtrace as _;
@@ -93,8 +95,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::Blocking;
use esp_hal::uart::Uart;
use esp_hal::Blocking;
use esp_radio::{init, Controller};
use esp_storage::FlashStorage;
use littlefs2::fs::{Allocation, Filesystem as lfs2Filesystem};
@@ -138,12 +140,6 @@ pub struct HAL<'a> {
pub board_hal: Box<dyn BoardInteraction<'a> + 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 +165,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<Detection> {
async fn detect_sensors(&mut self, _request: Detection) -> FatResult<Detection> {
bail!("Autodetection is only available on v4 HAL with CAN bus");
}
@@ -333,31 +329,31 @@ impl PlantHal {
);
let ota_data = mk_static!(
FlashRegion<MutexFlashStorage>,
ota_data.as_embedded_storage(flash_storage_2)
FlashRegion<RmwNorFlashStorage<&mut MutexFlashStorage>>,
ota_data.as_embedded_storage(mk_static!(
RmwNorFlashStorage<&mut MutexFlashStorage>,
RmwNorFlashStorage::new(flash_storage_2, mk_static!([u8; 4096], [0_u8; 4096]))
))
);
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 running = get_current_slot(&pt, &mut ota)?;
let target = next_partition(running)?;
info!("Currently running OTA slot: {running:?}");
info!("Updates will be stored in OTA slot: {target:?}");
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,
))?
.find_partition(PartitionType::App(AppPartitionSubType::Ota0))?
.context("Partition table invalid no ota0")?,
AppPartitionSubType::Ota1 => pt
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App(
AppPartitionSubType::Ota1,
))?
.find_partition(PartitionType::App(AppPartitionSubType::Ota1))?
.context("Partition table invalid no ota1")?,
_ => {
bail!("Invalid target partition");
@@ -401,8 +397,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(),
})?;
@@ -421,7 +417,7 @@ impl PlantHal {
current: running,
slot0_state: state_0,
slot1_state: state_1,
uart0
uart0,
};
//init,reset rtc memory depending on cause
@@ -551,9 +547,9 @@ impl PlantHal {
// initial_hal::create_initial_board(free_pins, config, esp)?
//}
//BoardVersion::V4 => {
v4_hal::create_v4(free_pins, esp, config, battery_interaction, rtc_module)
.await?;
//}
v4_hal::create_v4(free_pins, esp, config, battery_interaction, rtc_module)
.await?;
//}
//};
HAL { board_hal }
@@ -571,9 +567,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?,
}
}
};
@@ -584,7 +585,7 @@ impl PlantHal {
fn ota_state(
slot: AppPartitionSubType,
ota_data: &mut FlashRegion<MutexFlashStorage>,
ota_data: &mut FlashRegion<RmwNorFlashStorage<&mut MutexFlashStorage>>,
) -> 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]
@@ -600,56 +601,40 @@ fn ota_state(
OtaImageState::try_from(raw_state).unwrap_or(OtaImageState::Undefined)
}
fn get_current_slot_and_fix_ota_data(
ota: &mut Ota<MutexFlashStorage>,
state0: OtaImageState,
state1: OtaImageState,
fn get_current_slot(
pt: &PartitionTable,
ota: &mut Ota<RmwNorFlashStorage<&mut MutexFlashStorage>>,
) -> Result<AppPartitionSubType, 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
let booted = pt.booted_partition()?.ok_or(FatError::OTAError)?;
let booted_type = booted.partition_type();
let booted_ota_type = match booted_type {
PartitionType::App(subtype) => subtype,
_ => {
bail!("Booted partition is not an app partition");
}
_ => 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)?;
let expected_partition = ota.current_app_partition()?;
if expected_partition == booted_ota_type {
info!("Booted partition matches expected partition");
} else {
info!("Booted partition does not match expected partition, fixing ota entry");
ota.set_current_app_partition(booted_ota_type)?;
}
//we actually booted other slot, than partition table assumes
return Ok(ota.current_app_partition()?);
};
Ok(current)
let fixed = ota.current_app_partition()?;
let state = ota.current_ota_state();
info!("Expected partition: {expected_partition:?}, current partition: {booted_ota_type:?}, state: {state:?}");
if fixed != booted_ota_type {
bail!(
"Could not fix ota entry, booted partition is still not correct: {:?} != {:?}",
booted_ota_type,
fixed
);
}
Ok(booted_ota_type)
}
pub fn next_partition(current: AppPartitionSubType) -> FatResult<AppPartitionSubType> {
@@ -699,4 +684,3 @@ pub struct DetectionSensorResult {
sensor_a: bool,
sensor_b: bool,
}

View File

@@ -5,6 +5,7 @@ use embassy_sync::blocking_mutex::CriticalSectionMutex;
use embedded_storage::nor_flash::{ErrorType, NorFlash, ReadNorFlash};
use embedded_storage::ReadStorage;
use esp_storage::{FlashStorage, FlashStorageError};
use log::info;
#[derive(Clone)]
pub struct MutexFlashStorage {
@@ -52,6 +53,7 @@ impl NorFlash for MutexFlashStorage {
const ERASE_SIZE: usize = 4096;
fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> {
info!("Erasing flash from 0x{:x} to 0x{:x}", from, to);
self.inner
.lock(|f| NorFlash::erase(f.borrow_mut().deref_mut(), from, to))
}

View File

@@ -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<TwaiConfiguration<'static, Blocking>>
twai_config: Option<TwaiConfiguration<'static, Blocking>>,
}
pub(crate) async fn create_v4(
peripherals: FreePeripherals<'static>,
esp: Esp<'static>,
@@ -263,12 +261,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> {
@@ -377,69 +374,6 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
Ok(moistures)
}
async fn detect_sensors(&mut self, request: Detection) -> FatResult<Detection> {
self.can_power.set_high();
let config = self.twai_config.take().expect("twai config not set");
let mut twai = config.into_async().start();
Timer::after_millis(1000).await;
info!("Sending info messages now");
// Send a few test messages per potential sensor node
for plant in 0..PLANT_COUNT {
for sensor in [Sensor::A, Sensor::B] {
let detect = if sensor == Sensor::A {
request.plant[plant].sensor_a
} else {
request.plant[plant].sensor_b
};
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 can_buffer = [0_u8; 0];
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
.transmit_async(&frame)
.with_timeout(Duration::from_millis(3000))
.await;
match resu {
Ok(_) => {
}
Err(err) => {
info!(
"Error sending test message to plant {} sensor {sensor:?}: {err:?}", plant +1
);
}
}
} else {
info!("Error building CAN frame");
}
}
}
let mut moistures = Moistures::default();
let _ = wait_for_can_measurements(&mut twai, &mut moistures)
.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());
@@ -482,7 +416,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,8 +443,74 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
}
Ok(())
}
}
async fn detect_sensors(&mut self, request: Detection) -> FatResult<Detection> {
self.can_power.set_high();
let config = self.twai_config.take().expect("twai config not set");
let mut twai = config.into_async().start();
Timer::after_millis(1000).await;
info!("Sending info messages now");
// Send a few test messages per potential sensor node
for plant in 0..PLANT_COUNT {
for sensor in [Sensor::A, Sensor::B] {
let detect = if sensor == Sensor::A {
request.plant[plant].sensor_a
} else {
request.plant[plant].sensor_b
};
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 can_buffer = [0_u8; 0];
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
.transmit_async(&frame)
.with_timeout(Duration::from_millis(3000))
.await;
match resu {
Ok(_) => {}
Err(err) => {
info!(
"Error sending test message to plant {} sensor {sensor:?}: {err:?}",
plant + 1
);
}
}
} else {
info!("Error building CAN frame");
}
}
}
let mut moistures = Moistures::default();
let _ = wait_for_can_measurements(&mut twai, &mut moistures)
.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 wait_for_can_measurements(
as_async: &mut Twai<'_, Async>,
@@ -538,10 +538,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 {