Skip to main content

dmx_interface/
storage.rs

1//! NVS persistent storage for device configuration and crash reports.
2//!
3//! Wraps `esp-nvs` behind a process-wide singleton so the panic handler can
4//! persist a crash snapshot even though the FLASH peripheral is consumed at
5//! init time. Every operation runs synchronously (no `await` inside) and is
6//! serialized through [`NVS_BUSY`], which lets the panic handler decide in
7//! one load whether the flash is safe to touch right now.
8
9extern crate alloc;
10
11use alloc::vec::Vec;
12use core::cell::{Cell, RefCell};
13
14use critical_section::Mutex;
15use esp_hal::peripherals::FLASH;
16use esp_nvs::{Key, Nvs};
17use esp_storage::FlashStorage;
18
19use crate::config::Config;
20
21/// NVS partition offset (default for ESP32-S2).
22const NVS_OFFSET: usize = 0x9000;
23
24/// NVS partition size (default for ESP32-S2: 24 KiB).
25const NVS_SIZE: usize = 0x6000;
26
27/// NVS namespace for config storage.
28const NS_CONFIG: Key = Key::from_str("config");
29
30/// NVS key for the config blob.
31const KEY_CFG: Key = Key::from_str("cfg");
32
33/// NVS namespace for diagnostics.
34const NS_DIAG: Key = Key::from_str("diag");
35
36/// NVS key for the persisted last-panic snapshot.
37const KEY_PANIC: Key = Key::from_str("last_panic");
38
39/// Concrete storage type: NVS on the internal flash.
40type Storage = Nvs<FlashStorage<'static>>;
41
42/// The global handle, set once by [`init`].
43static NVS: Mutex<RefCell<Option<Storage>>> = Mutex::new(RefCell::new(None));
44
45/// Set while any NVS operation is in flight.
46///
47/// * The panic handler skips its durable write when the flag is set (the RTC
48///   copy still records the crash) — this both avoids touching a half-used
49///   handle and prevents a re-entrant panic from looping on a failing flash.
50/// * [`with_nvs`] bails out when it sees the flag already set, so a task
51///   interrupted mid-operation can never be re-entered from interrupt/panic
52///   context either.
53///
54/// A `critical_section`-guarded cell instead of an `AtomicBool`: xtensa has
55/// no compare-and-swap, so `AtomicBool::swap` does not exist on this target.
56static NVS_BUSY: Mutex<Cell<bool>> = Mutex::new(Cell::new(false));
57
58/// Initialize the global NVS handle. Takes ownership of the FLASH peripheral.
59///
60/// Must be called once at startup (after heap init) before any load/save
61/// operation. A failed init leaves storage disabled; operations then fall
62/// back to defaults instead of panicking.
63pub fn init(flash: FLASH<'static>) {
64    match Nvs::new(NVS_OFFSET, NVS_SIZE, FlashStorage::new(flash)) {
65        Ok(nvs) => {
66            critical_section::with(|cs| {
67                *NVS.borrow(cs).borrow_mut() = Some(nvs);
68            });
69            log::info!("NVS storage initialized ({NVS_SIZE} bytes)");
70        }
71        Err(e) => log::error!("NVS init failed, storage disabled: {e:?}"),
72    }
73}
74
75/// Run `f` against the global handle if storage is up and the flash is idle.
76///
77/// Synchronous by contract: `f` must not `await`, otherwise the busy flag
78/// would stay set for the whole suspension. The operation runs inside a
79/// critical section to scope the `RefCell` borrow (`critical_section::Mutex`
80/// loans cannot escape the closure). Interrupt masking over the long flash
81/// calls happens anyway: `esp-storage` takes its own interrupt lock around
82/// every ROM spiflash call (verified via `cargo tree` — feature
83/// `critical-section` is on); the outer section only additionally covers the
84/// bookkeeping gaps between calls. Whether a save disturbs a live AP link
85/// long enough to matter is a hardware check (see `TODO.md`).
86fn with_nvs<R>(f: impl FnOnce(&mut Storage) -> R) -> Option<R> {
87    if busy_swap() {
88        return None;
89    }
90    let out = critical_section::with(|cs| NVS.borrow(cs).borrow_mut().as_mut().map(f));
91    busy_clear();
92    out
93}
94
95/// Atomically set the busy flag and return its previous value.
96fn busy_swap() -> bool {
97    critical_section::with(|cs| {
98        let flag = NVS_BUSY.borrow(cs);
99        let prev = flag.get();
100        flag.set(true);
101        prev
102    })
103}
104
105/// Clear the busy flag.
106fn busy_clear() {
107    critical_section::with(|cs| NVS_BUSY.borrow(cs).set(false));
108}
109
110/// Load the persisted device configuration.
111///
112/// Returns [`Config::default`] if nothing is stored, storage is down, or
113/// deserialization fails.
114pub fn load() -> Config {
115    match with_nvs(|nvs| nvs.get::<Vec<u8>>(&NS_CONFIG, &KEY_CFG)) {
116        Some(Ok(bytes)) => match Config::from_bytes(&bytes) {
117            Ok(config) => {
118                log::info!("Config loaded from NVS ({} bytes)", bytes.len());
119                config
120            }
121            Err(e) => {
122                log::warn!("Config deserialization failed: {e:?}, using defaults");
123                Config::default()
124            }
125        },
126        Some(Err(e)) => {
127            log::warn!("NVS read failed: {e:?}, using defaults");
128            Config::default()
129        }
130        None => {
131            log::warn!("NVS unavailable, using default config");
132            Config::default()
133        }
134    }
135}
136
137/// Save the device configuration to NVS (serialized via `postcard`).
138///
139/// Returns `false` when serialization or the NVS write fails — callers
140/// must not report success (or apply the config) in that case.
141pub fn save(config: &Config) -> bool {
142    match config.to_bytes() {
143        Ok(bytes) => match with_nvs(|nvs| nvs.set(&NS_CONFIG, &KEY_CFG, bytes.as_slice())) {
144            Some(Ok(())) => {
145                log::info!("Config saved to NVS ({} bytes)", bytes.len());
146                true
147            }
148            Some(Err(e)) => {
149                log::warn!("NVS write failed: {e:?}");
150                false
151            }
152            None => {
153                log::warn!("NVS busy or unavailable, config not saved");
154                false
155            }
156        },
157        Err(e) => {
158            log::warn!("Config serialization failed: {e:?}");
159            false
160        }
161    }
162}
163
164/// Clear all config data from NVS (reverts to defaults on next load).
165///
166/// Returns `false` when storage is busy or the delete fails — callers
167/// must not reboot into a "factory reset" that did not happen.
168pub fn clear() -> bool {
169    match with_nvs(|nvs| nvs.delete(&NS_CONFIG, &KEY_CFG)) {
170        Some(Ok(())) => {
171            log::info!("Config cleared from NVS");
172            true
173        }
174        Some(Err(e)) => {
175            log::warn!("NVS clear failed: {e:?}");
176            false
177        }
178        None => {
179            log::warn!("NVS busy or unavailable, nothing cleared");
180            false
181        }
182    }
183}
184
185/// Persist the last-panic snapshot (overwrites the previous one).
186///
187/// Returns `false` when storage is down or the flash is busy — the caller
188/// then relies on the RTC copy alone.
189pub(crate) fn persist_panic(data: &[u8]) -> bool {
190    match with_nvs(|nvs| nvs.set(&NS_DIAG, &KEY_PANIC, data)) {
191        Some(Ok(())) => true,
192        Some(Err(e)) => {
193            log::error!("Panic snapshot NVS write failed: {e:?}");
194            false
195        }
196        None => false,
197    }
198}
199
200/// Load the persisted last-panic snapshot, if any.
201///
202/// Boot path only (allocates), so the heap must be up.
203pub(crate) fn load_panic() -> Option<Vec<u8>> {
204    with_nvs(|nvs| nvs.get::<Vec<u8>>(&NS_DIAG, &KEY_PANIC)).and_then(Result::ok)
205}