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