Skip to main content

device_envoy_esp/
flash_block.rs

1//! A device abstraction for type-safe persistent storage in flash memory.
2//!
3//! This module provides a generic flash block storage system that allows storing any
4//! `serde`-compatible type in ESP's internal flash memory.
5//!
6//! See [`FlashBlockEsp`] for details and usage examples.
7#![cfg_attr(not(target_os = "none"), allow(dead_code))]
8
9#[cfg(target_os = "none")]
10use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
11#[cfg(target_os = "none")]
12use embassy_sync::blocking_mutex::Mutex;
13#[cfg(target_os = "none")]
14use embedded_storage::nor_flash::{NorFlash, ReadNorFlash};
15#[cfg(target_os = "none")]
16use serde::{Deserialize, Serialize};
17#[cfg(target_os = "none")]
18use static_cell::StaticCell;
19
20#[cfg(target_os = "none")]
21use crate::{Error, Result};
22#[cfg(target_os = "none")]
23use device_envoy_core::flash_block::{
24    self as core_flash, FlashBlock as CoreFlashBlock, FlashBlockError, FlashDevice,
25};
26
27pub use device_envoy_core::flash_block::FlashBlock;
28
29#[cfg(target_os = "none")]
30const FLASH_BLOCK_SIZE: usize = <esp_storage::FlashStorage<'static> as NorFlash>::ERASE_SIZE;
31#[cfg(target_os = "none")]
32const FLASH_BLOCK_SIZE_U32: u32 = FLASH_BLOCK_SIZE as u32;
33#[cfg(target_os = "none")]
34const DEFAULT_FLASH_REGION_BYTES: u32 = 16 * FLASH_BLOCK_SIZE_U32;
35
36// Local adapter — wraps esp_storage::FlashStorage so core's FlashDevice trait can be
37// implemented for a type defined in this crate (required by the orphan rule).
38#[cfg(target_os = "none")]
39struct EspFlashAdapter<'a>(&'a mut esp_storage::FlashStorage<'static>);
40
41#[cfg(target_os = "none")]
42impl FlashDevice for EspFlashAdapter<'_> {
43    type Error = esp_storage::FlashStorageError;
44
45    fn read(
46        &mut self,
47        offset: u32,
48        bytes: &mut [u8],
49    ) -> Result<(), esp_storage::FlashStorageError> {
50        ReadNorFlash::read(self.0, offset, bytes)
51    }
52
53    fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), esp_storage::FlashStorageError> {
54        NorFlash::write(self.0, offset, bytes)
55    }
56
57    fn erase(&mut self, from: u32, to: u32) -> Result<(), esp_storage::FlashStorageError> {
58        NorFlash::erase(self.0, from, to)
59    }
60}
61
62#[cfg(target_os = "none")]
63fn convert_flash_block_error(e: FlashBlockError<esp_storage::FlashStorageError>) -> Error {
64    match e {
65        FlashBlockError::Io(err) => Error::FlashStorage(err),
66        FlashBlockError::FormatError => Error::FormatError,
67        FlashBlockError::StorageCorrupted => Error::StorageCorrupted,
68    }
69}
70
71#[cfg(target_os = "none")]
72#[derive(Clone, Copy, Debug, Eq, PartialEq)]
73enum FlashRegionRequest {
74    Tail { byte_len: u32 },
75}
76
77#[cfg(target_os = "none")]
78#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79struct ResolvedFlashRegion {
80    start_offset: u32,
81    block_count: u32,
82}
83
84#[cfg(target_os = "none")]
85impl FlashRegionRequest {
86    fn resolve(self, flash_capacity: u32) -> Result<ResolvedFlashRegion> {
87        let Self::Tail { byte_len } = self;
88        if byte_len == 0 || byte_len > flash_capacity {
89            return Err(Error::InvalidFlashRegion);
90        }
91        let start_offset = flash_capacity - byte_len;
92
93        if start_offset % FLASH_BLOCK_SIZE_U32 != 0 || byte_len % FLASH_BLOCK_SIZE_U32 != 0 {
94            return Err(Error::InvalidFlashRegion);
95        }
96        let end_offset = start_offset
97            .checked_add(byte_len)
98            .ok_or(Error::InvalidFlashRegion)?;
99        if end_offset > flash_capacity {
100            return Err(Error::InvalidFlashRegion);
101        }
102        Ok(ResolvedFlashRegion {
103            start_offset,
104            block_count: byte_len / FLASH_BLOCK_SIZE_U32,
105        })
106    }
107}
108
109#[cfg(target_os = "none")]
110struct FlashManager {
111    flash_storage:
112        Mutex<CriticalSectionRawMutex, core::cell::RefCell<esp_storage::FlashStorage<'static>>>,
113    next_block: core::sync::atomic::AtomicU32,
114    requested_region: FlashRegionRequest,
115    resolved_region: ResolvedFlashRegion,
116}
117
118#[cfg(target_os = "none")]
119impl FlashManager {
120    fn new(
121        flash: esp_hal::peripherals::FLASH<'static>,
122        requested_region: FlashRegionRequest,
123    ) -> Result<Self> {
124        let flash_storage = esp_storage::FlashStorage::new(flash);
125        let flash_capacity = ReadNorFlash::capacity(&flash_storage) as u32;
126        let resolved_region = requested_region.resolve(flash_capacity)?;
127        Ok(Self {
128            flash_storage: Mutex::new(core::cell::RefCell::new(flash_storage)),
129            next_block: core::sync::atomic::AtomicU32::new(0),
130            requested_region,
131            resolved_region,
132        })
133    }
134
135    fn with_flash<R>(
136        &self,
137        f: impl FnOnce(&mut esp_storage::FlashStorage<'static>) -> Result<R>,
138    ) -> Result<R> {
139        self.flash_storage.lock(|flash_storage| {
140            let mut flash_storage_ref = flash_storage.borrow_mut();
141            f(&mut flash_storage_ref)
142        })
143    }
144
145    fn reserve<const N: usize>(&'static self) -> Result<[FlashBlockEsp; N]> {
146        let start_block = self
147            .next_block
148            .fetch_add(N as u32, core::sync::atomic::Ordering::SeqCst);
149        let end_block = start_block
150            .checked_add(N as u32)
151            .ok_or(Error::IndexOutOfBounds)?;
152        if end_block > self.resolved_region.block_count {
153            self.next_block
154                .fetch_sub(N as u32, core::sync::atomic::Ordering::SeqCst);
155            return Err(Error::IndexOutOfBounds);
156        }
157
158        Ok(core::array::from_fn(|block_index| FlashBlockEsp {
159            manager: self,
160            block_id: start_block + block_index as u32,
161        }))
162    }
163
164    fn block_offset(&self, block_id: u32) -> Result<u32> {
165        if block_id >= self.resolved_region.block_count {
166            return Err(Error::IndexOutOfBounds);
167        }
168        let reverse_index = self.resolved_region.block_count - 1 - block_id;
169        Ok(self.resolved_region.start_offset + reverse_index * FLASH_BLOCK_SIZE_U32)
170    }
171}
172
173#[cfg(target_os = "none")]
174struct FlashBlockEspStatic {
175    manager_cell: StaticCell<FlashManager>,
176    manager_ref: Mutex<CriticalSectionRawMutex, core::cell::RefCell<Option<&'static FlashManager>>>,
177}
178
179#[cfg(target_os = "none")]
180impl FlashBlockEspStatic {
181    const fn new() -> Self {
182        Self {
183            manager_cell: StaticCell::new(),
184            manager_ref: Mutex::new(core::cell::RefCell::new(None)),
185        }
186    }
187
188    fn manager(
189        &'static self,
190        flash: esp_hal::peripherals::FLASH<'static>,
191        requested_region: FlashRegionRequest,
192    ) -> Result<&'static FlashManager> {
193        self.manager_ref.lock(|manager_slot| {
194            let mut manager_slot = manager_slot.borrow_mut();
195            if let Some(manager) = *manager_slot {
196                if manager.requested_region != requested_region {
197                    return Err(Error::FlashRegionMismatch);
198                }
199                return Ok(manager);
200            }
201
202            let manager_ref = self
203                .manager_cell
204                .init(FlashManager::new(flash, requested_region)?);
205            *manager_slot = Some(manager_ref);
206            Ok(manager_ref)
207        })
208    }
209}
210
211#[cfg(target_os = "none")]
212/// A device abstraction for type-safe persistent storage in flash memory.
213///
214/// `FlashBlockEsp` provides a generic flash-block storage system for ESP,
215/// allowing you to store any `serde`-compatible type in the device's internal flash.
216///
217/// Use [`FlashBlockEsp::new_array`] to allocate one or more blocks. Block operations like
218/// [`load`](FlashBlock::load), [`save`](FlashBlock::save), and
219/// [`clear`](FlashBlock::clear) are provided by [`FlashBlock`], so bring the trait into
220/// scope:
221///
222/// `use device_envoy_esp::flash_block::FlashBlock as _;`
223///
224/// # Features
225///
226/// - **Type safety**: Hash-based type checking prevents reading data written under a
227///   different Rust type name. The hash is derived from the full type path
228///   (for example, `app1::BootCounter`). **Trying to read a different type
229///   returns `Ok(None)`**. Structural changes (adding or removing fields) do not
230///   change the hash, but may cause deserialization to fail and return an error.
231/// - **Postcard serialization**: A compact, `no_std`-friendly binary format.
232///
233/// # Block allocation
234///
235/// Conceptually, flash is treated as an array of fixed-size erase blocks counted from
236/// the end of the configured region backward. Your code can split that array using
237/// destructuring assignment and hand individual blocks to subsystems that need
238/// persistent storage.
239///
240/// ⚠️ **Warning**: ESP firmware and user data share the same flash device.
241/// Allocating too many blocks can overwrite your firmware.
242///
243/// # Example
244///
245/// ```rust,no_run
246/// # #![no_std]
247/// # #![no_main]
248/// use device_envoy_esp::{Result, init_and_start, flash_block::{FlashBlockEsp, FlashBlock as _}};
249///
250/// #[derive(serde::Serialize, serde::Deserialize, Clone)]
251/// struct WifiPersistedState {
252///     ssid: heapless::String<32>,
253///     password: heapless::String<64>,
254///     timezone_offset_minutes: i32,
255/// }
256///
257/// # async fn example() -> Result<core::convert::Infallible> {
258/// init_and_start!(p);
259/// let [mut wifi_persisted_state_flash_block, mut fields_flash_block] =
260///     FlashBlockEsp::new_array::<2>(p.FLASH)?;
261///
262/// let wifi_persisted_state = wifi_persisted_state_flash_block.load::<WifiPersistedState>()?;
263/// if wifi_persisted_state.is_none() {
264///     let wifi_persisted_state = WifiPersistedState {
265///         ssid: heapless::String::new(),
266///         password: heapless::String::new(),
267///         timezone_offset_minutes: 0,
268///     };
269///     wifi_persisted_state_flash_block.save(&wifi_persisted_state)?;
270/// }
271///
272/// fields_flash_block.clear()?;
273/// # core::future::pending().await
274/// # }
275/// ```
276
277#[cfg(target_os = "none")]
278#[derive(Clone, Copy)]
279pub struct FlashBlockEsp {
280    manager: &'static FlashManager,
281    block_id: u32,
282}
283
284#[cfg(target_os = "none")]
285impl CoreFlashBlock for FlashBlockEsp {
286    type Error = Error;
287
288    fn load<T>(&mut self) -> Result<Option<T>>
289    where
290        T: Serialize + for<'de> Deserialize<'de>,
291    {
292        let block_offset = self.manager.block_offset(self.block_id)?;
293        self.manager.with_flash(|flash_storage| {
294            let mut adapter = EspFlashAdapter(flash_storage);
295            core_flash::load_block::<{ FLASH_BLOCK_SIZE }, T, _>(&mut adapter, block_offset)
296                .map_err(convert_flash_block_error)
297        })
298    }
299
300    fn save<T>(&mut self, value: &T) -> Result<()>
301    where
302        T: Serialize + for<'de> Deserialize<'de>,
303    {
304        let block_offset = self.manager.block_offset(self.block_id)?;
305        self.manager.with_flash(|flash_storage| {
306            let mut adapter = EspFlashAdapter(flash_storage);
307            core_flash::save_block::<{ FLASH_BLOCK_SIZE }, _, _>(&mut adapter, block_offset, value)
308                .map_err(convert_flash_block_error)
309        })
310    }
311
312    fn clear(&mut self) -> Result<()> {
313        let block_offset = self.manager.block_offset(self.block_id)?;
314        self.manager.with_flash(|flash_storage| {
315            let mut adapter = EspFlashAdapter(flash_storage);
316            core_flash::clear_block::<{ FLASH_BLOCK_SIZE }, _>(&mut adapter, block_offset)
317                .map_err(convert_flash_block_error)
318        })
319    }
320}
321
322#[cfg(target_os = "none")]
323impl FlashBlockEsp {
324    /// Reserve `N` blocks in the default tail region.
325    pub fn new_array<const N: usize>(
326        flash: esp_hal::peripherals::FLASH<'static>,
327    ) -> Result<[FlashBlockEsp; N]> {
328        Self::new_array_with_request(
329            flash,
330            FlashRegionRequest::Tail {
331                byte_len: DEFAULT_FLASH_REGION_BYTES,
332            },
333        )
334    }
335
336    fn new_array_with_request<const N: usize>(
337        flash: esp_hal::peripherals::FLASH<'static>,
338        requested_region: FlashRegionRequest,
339    ) -> Result<[FlashBlockEsp; N]> {
340        static FLASH_BLOCK_ESP_STATIC: FlashBlockEspStatic = FlashBlockEspStatic::new();
341        let manager = FLASH_BLOCK_ESP_STATIC.manager(flash, requested_region)?;
342        manager.reserve::<N>()
343    }
344}