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}