Skip to main content

embedded_wg_display/storage/
mod.rs

1//! NVS-backed persistent storage for widget binaries, system config, and WiFi credentials.
2//!
3//! Two namespaces used:
4//! - `"config"` — for all config related data. Stores WIFI Name and Password and [`SystemConfiguration`].
5//! - `"wasm"` — precompiled widget WASM binaries, name hashed to fit NVS key length limit with [`Hasher`].
6use crate::util::hasher::Hasher;
7use common::models::{SystemConfiguration, WidgetInstallationData};
8use defmt::info;
9use esp_bootloader_esp_idf::partitions;
10use esp_hal::peripherals::FLASH;
11use esp_hal::peripherals::SHA;
12use esp_nvs::{Key, Nvs, error::Error as NvsError};
13use esp_storage::{FlashStorage, FlashStorageError};
14
15pub struct Storage<'d> {
16    nvs: Nvs<FlashStorage<'d>>,
17    hasher: Hasher<'d>,
18    config_updated: bool,
19}
20
21/// Errors returned by storage operations.
22#[derive(Debug, defmt::Format)]
23pub enum StorageError {
24    /// Low-level flash read/write error.
25    Flash(esp_storage::FlashStorageError),
26    /// Error reading or accessing the NVS partition table.
27    Partition(partitions::Error),
28    /// The `"storage"` partition was not found in the partition table.
29    PartitionNotFound,
30    /// NVS key read/write error.
31    Nvs(NvsError),
32}
33
34impl From<FlashStorageError> for StorageError {
35    fn from(e: FlashStorageError) -> Self {
36        StorageError::Flash(e)
37    }
38}
39
40impl From<partitions::Error> for StorageError {
41    fn from(e: partitions::Error) -> Self {
42        StorageError::Partition(e)
43    }
44}
45
46impl From<NvsError> for StorageError {
47    fn from(e: NvsError) -> Self {
48        StorageError::Nvs(e)
49    }
50}
51
52impl<'d> Storage<'d> {
53    fn wasm_key_from_name(&mut self, name: &str) -> Key {
54        // create ascii only hash for widget name
55        let digest = self.hasher.hash(name);
56        let mut key_bytes = [b'0'; 15];
57        const HEX: &[u8; 16] = b"0123456789abcdef";
58
59        for i in 0..7 {
60            key_bytes[2 * i] = HEX[(digest[i] >> 4) as usize];
61            key_bytes[2 * i + 1] = HEX[(digest[i] & 0x0f) as usize];
62        }
63        key_bytes[14] = HEX[(digest[7] >> 4) as usize];
64
65        Key::from_array(&key_bytes)
66    }
67
68    /// Creates and initialises the storage handle.
69    /// Reads the partition table from flash to find the `"storage"` partition and init NVS with the correct offset and size of that partition.
70    pub fn new(flash: FLASH<'d>, sha_peripherals: SHA<'d>) -> Result<Self, StorageError> {
71        let mut flash_storage = FlashStorage::new(flash).multicore_auto_park();
72
73        // read partition table using esp_bootloader_esp_idf
74        // heap-allocated (→ PSRAM) to avoid large stack frame during init
75        let mut partition_table_buffer =
76            alloc::boxed::Box::new([0u8; partitions::PARTITION_TABLE_MAX_LEN]);
77        let partition_table =
78            partitions::read_partition_table(&mut flash_storage, &mut *partition_table_buffer)?;
79
80        // list partitions
81        defmt::info!("Partition table:");
82        for partition in partition_table.iter() {
83            defmt::info!(
84                "  {}: offset=0x{:x}, size=0x{:x}",
85                partition.label_as_str(),
86                partition.offset(),
87                partition.len()
88            );
89        }
90
91        // find the combined storage partition
92        let storage = partition_table
93            .iter()
94            .find(|p| p.label_as_str() == "storage")
95            .ok_or(StorageError::PartitionNotFound)?;
96
97        let nvs = Nvs::new(
98            storage.offset() as usize,
99            storage.len() as usize,
100            flash_storage,
101        )?;
102
103        Ok(Self {
104            nvs,
105            hasher: Hasher::new(sha_peripherals),
106            config_updated: false,
107        })
108    }
109
110    /// Persists the system configuration to NVS.
111    ///
112    /// The write is skipped if there are no changes in the config to avoid flash wear.
113    pub fn save_system_config(
114        &mut self,
115        system_config: &SystemConfiguration,
116    ) -> Result<(), StorageError> {
117        // only save if config changed to avoid flash wear
118        if let Ok(current_config) = self.get_system_config()
119            && current_config == *system_config
120        {
121            info!("System config unchanged, not saving to flash");
122            return Ok(());
123        }
124
125        let value = serde_json::to_string(system_config)
126            .map_err(|_| StorageError::Nvs(NvsError::FlashError))?;
127        self.config_set("system_config", &value)?;
128        self.config_updated = true;
129        Ok(())
130    }
131
132    /// Reads and deserialises the system configuration, returns a default [`SystemConfiguration`] if no config has been saved yet (First boot).
133    pub fn get_system_config(&mut self) -> Result<SystemConfiguration, StorageError> {
134        let value: alloc::string::String = self.config_get("system_config")?;
135        let config: SystemConfiguration =
136            serde_json::from_str(&value).map_err(|_| StorageError::Nvs(NvsError::FlashError))?;
137        Ok(config)
138    }
139
140    /// Only returns the sytem config if there has been a change since the last call to this function, otherwise returns `None`.
141    /// used by [Renderer](crate::renderer::Renderer) to detect config changes.
142    pub fn get_system_config_change(&mut self) -> Option<SystemConfiguration> {
143        if self.config_updated {
144            self.config_updated = false;
145            match self.get_system_config() {
146                Ok(config) => Some(config),
147                Err(err) => {
148                    info!("Error getting updated config: {:?}", err);
149                    None
150                }
151            }
152        } else {
153            None
154        }
155    }
156
157    /// Writes a widget WASM binary to NVS **and** adds it to the system config in one call.
158    pub fn save_compiled_widget(
159        &mut self,
160        widget_metadata: WidgetInstallationData,
161        data: &[u8],
162    ) -> Result<(), StorageError> {
163        self.wasm_write(&widget_metadata.name, data)?;
164        let mut config = self.get_system_config()?;
165        config.widgets.push(widget_metadata);
166        self.save_system_config(&config)?;
167        Ok(())
168    }
169
170    /// Removes a widget's WASM binary from NVS and deletes its entry from the system config.
171    pub fn deinstall_widget(&mut self, name: &str) -> Result<(), StorageError> {
172        // self.wasm_read(name)?; // check if widget exists
173        self.wasm_delete(name)?; // remove widget data
174        let mut config = self.get_system_config()?;
175        config.widgets.retain(|w| w.name != name);
176        self.save_system_config(&config)?;
177        Ok(())
178    }
179
180    /// Stores a key-value string in the `"config"` NVS namespace.
181    pub fn config_set(&mut self, key: &str, value: &str) -> Result<(), StorageError> {
182        info!("Setting config for key '{}'", key);
183        let ns = Key::from_str("config");
184        let k = Key::from_str(key);
185        self.nvs.set(&ns, &k, value)?;
186        Ok(())
187    }
188
189    /// Reads a key-value string from the `"config"` NVS namespace.
190    pub fn config_get(&mut self, key: &str) -> Result<alloc::string::String, StorageError> {
191        info!("Getting config for key '{}'", key);
192        let ns = Key::from_str("config");
193        let k = Key::from_str(key);
194        Ok(self.nvs.get(&ns, &k)?)
195    }
196
197    /// Writes a raw WASM binary to the `"wasm"` NVS namespace.
198    pub fn wasm_write(&mut self, name: &str, data: &[u8]) -> Result<(), StorageError> {
199        let key = self.wasm_key_from_name(name);
200        let ns = Key::from_str("wasm");
201        info!(
202            "Writing WASM binary with name: '{}' and key: {:?}",
203            name, key
204        );
205        self.nvs.set(&ns, &key, data)?;
206        Ok(())
207    }
208
209    /// Reads a previously stored WASM binary from the `"wasm"` NVS namespace.
210    pub fn wasm_read(&mut self, name: &str) -> Result<alloc::vec::Vec<u8>, StorageError> {
211        let key = self.wasm_key_from_name(name);
212        let ns = Key::from_str("wasm");
213        info!(
214            "Reading WASM binary with name: '{}' and key: {:?}",
215            name, key
216        );
217        Ok(self.nvs.get(&ns, &key)?)
218    }
219
220    /// Deletes a WASM binary from the `"wasm"` NVS namespace.
221    pub fn wasm_delete(&mut self, name: &str) -> Result<(), StorageError> {
222        let key = self.wasm_key_from_name(name);
223        let ns = Key::from_str("wasm");
224        info!(
225            "Deleting WASM binary with name: '{}' and key: {:?}",
226            name, key
227        );
228        self.nvs.delete(&ns, &key)?;
229        Ok(())
230    }
231
232    /// Returns the names of all installed widgets from the system config.
233    #[allow(dead_code)]
234    pub fn list_widgets(&mut self) -> Result<alloc::vec::Vec<alloc::string::String>, StorageError> {
235        let config = self.get_system_config()?;
236        Ok(config.widgets.iter().map(|w| w.name.clone()).collect())
237    }
238}