diff --git a/Cargo.lock b/Cargo.lock index a07b77b19..429d47071 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1205,8 +1205,10 @@ version = "0.1.0" dependencies = [ "embedded-io", "heapless 0.8.0", + "log", "menu", "miniconf", + "postcard", "yafnv", ] @@ -1582,9 +1584,9 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "yafnv" -version = "2.0.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66aad9090f952997c6570d94039d80b5b5e26d182c5f39ef7b05d64535fb5f88" +checksum = "a98cd19b5e3fbcda406d58efc9f5cb2d6e82e381708d52f357b3b3cfb19559b1" dependencies = [ "num-traits", ] diff --git a/serial-settings/Cargo.toml b/serial-settings/Cargo.toml index ac21f5049..c081b88eb 100644 --- a/serial-settings/Cargo.toml +++ b/serial-settings/Cargo.toml @@ -6,8 +6,10 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -miniconf = { version = "0.13", features = ["json-core"] } +miniconf = { version = "0.13", features = ["json-core", "postcard"] } menu = { version = "0.5", features = ["echo"] } heapless = "0.8" embedded-io = "0.6" -yafnv = "2.0" +yafnv = "3.0" +postcard = "1" +log = { version = "0.4", features = ["max_level_trace", "release_max_level_info"] } diff --git a/serial-settings/src/interface.rs b/serial-settings/src/interface.rs index dc4162fff..f3441016f 100644 --- a/serial-settings/src/interface.rs +++ b/serial-settings/src/interface.rs @@ -36,7 +36,7 @@ where if let Ok(true) = self.0.write_ready() { self.0.write(buf) } else { - Ok(0) + Ok(buf.len()) // discard! } } diff --git a/serial-settings/src/lib.rs b/serial-settings/src/lib.rs index c729e776b..d4223d348 100644 --- a/serial-settings/src/lib.rs +++ b/serial-settings/src/lib.rs @@ -1,7 +1,7 @@ //! Persistent Settings Management Serial Interface //! //! # Description -//! This crate provides a simple means to load, configure, and save device settings over a serial +//! This crate provides a simple means to load, configure, and store device settings over a serial //! (i.e. text-based) interface. It is ideal to be used with serial ports and terminal emulators, //! and exposes a simple way to allow users to configure device operation. //! @@ -19,11 +19,10 @@ //! ``` //!> help //! AVAILABLE ITEMS: -//! list -//! get -//! set -//! save [item] -//! clear [item] +//! get [path] +//! set +//! store [path] +//! clear [path] //! platform //! help [ ] //! @@ -33,10 +32,10 @@ //! > plaform service //! Service data not available //! -//! > list +//! > get //! Available settings: -//! /broker: "test" [default: "mqtt"] -//! /id: "04-91-62-d2-a8-6f" [default: "04-91-62-d2-a8-6f"] +//! /broker: "test" [default: "mqtt"] [not stored] +//! /id: "04-91-62-d2-a8-6f" [default: "04-91-62-d2-a8-6f"] [not stored] //! ``` //! //! # Design @@ -51,60 +50,63 @@ use embedded_io::{ErrorType, Read, ReadReady, Write}; use heapless::String; -use miniconf::{JsonCoreSlash, Path, TreeKey}; +use miniconf::{JsonCoreSlash, Path, Postcard, Traversal, TreeKey}; mod interface; - pub use interface::BestEffortInterface; /// Specifies the API required for objects that are used as settings with the serial terminal /// interface. pub trait Settings: - for<'a> JsonCoreSlash<'a, Y> + Clone + for<'de> JsonCoreSlash<'de, Y> + for<'de> Postcard<'de, Y> + Clone { /// Reset the settings to their default values. fn reset(&mut self) {} } -pub trait Platform: Sized { +/// Platform support for serial settings. +/// +/// Covers platform-specific commands, persistent key-value storage and the +/// Read/Write interface for interaction. +/// +/// Assuming there are no unit fields in the `Settings`, the empty value can be +/// used to mark the "cleared" state. +pub trait Platform { /// This type specifies the interface to the user, for example, a USB CDC-ACM serial port. type Interface: embedded_io::Read + embedded_io::ReadReady + embedded_io::Write; - /// Specifies the settings that are used on the device. + type Error: core::fmt::Debug; + type Settings: Settings; - /// `save()` Error type - type Error: core::fmt::Debug; + /// Fetch a value from persisten storage + fn fetch<'a>( + &mut self, + buf: &'a mut [u8], + key: &[u8], + ) -> Result, Self::Error>; - /// Save the settings to storage - fn save( + /// Store a value to persistent storage + fn store( &mut self, - buffer: &mut [u8], - key: Option<&str>, - settings: &Self::Settings, + buf: &mut [u8], + key: &[u8], + value: &[u8], ) -> Result<(), Self::Error>; + /// Remove a key from storage. + fn clear(&mut self, buf: &mut [u8], key: &[u8]) -> Result<(), Self::Error>; + /// Execute a platform specific command. fn cmd(&mut self, cmd: &str); - /// Handle clearing a settings key. - /// - /// # Note - /// The run-time setting will have already been updated when this is called. This is intended - /// to remove settings from permanent storage if necessary. - /// - /// # Arguments - /// * `buffer` The element serialization buffer. - /// * `key` The name of the setting to be cleared. If `None`, all settings are cleared. - fn clear(&mut self, buffer: &mut [u8], key: Option<&str>); - /// Return a mutable reference to the `Interface`. fn interface_mut(&mut self) -> &mut Self::Interface; } -struct Interface<'a, P: Platform, const Y: usize> { +struct Interface<'a, P, const Y: usize> { platform: P, buffer: &'a mut [u8], updated: bool, @@ -122,163 +124,384 @@ impl<'a, P: Platform, const Y: usize> Interface<'a, P, Y> { interface.platform.cmd(key) } - fn handle_list( - _menu: &menu::Menu, - _item: &menu::Item, - _args: &[&str], + fn iter_root( + key: Option<&str>, interface: &mut Self, settings: &mut P::Settings, - ) { + mut func: F, + ) where + F: FnMut( + Path<&str, '/'>, + &mut Self, + &mut P::Settings, + &mut P::Settings, + ), + { + let mut iter = P::Settings::nodes::, '/'>>(); + if let Some(key) = key { + match iter.root(&Path::<_, '/'>::from(key)) { + Ok(it) => iter = it, + Err(e) => { + writeln!(interface, "Failed to locate `{key}`: {e}") + .unwrap(); + return; + } + }; + } + let mut defaults = settings.clone(); defaults.reset(); - - for path in P::Settings::nodes::, '/'>>() { - match path { - Err(depth) => writeln!( - interface, - "Failed to get path: no space at depth {depth}" - ), - Ok((path, _node)) => { - let value = match settings - .get_json_by_key(&path, interface.buffer) - { - Err(e) => { - writeln!( - interface, - "Failed to read {}: {e}", - path.as_str() - ) - .unwrap(); - continue; - } - Ok(len) => { - core::str::from_utf8(&interface.buffer[..len]) - .unwrap() - } - }; - - write!( - interface.platform.interface_mut(), - "{}: {value}", - path.as_str(), + for key in iter { + match key { + Ok((key, node)) => { + debug_assert!(node.is_leaf()); + func( + key.as_str().into(), + interface, + settings, + &mut defaults, + ) + } + Err(depth) => { + writeln!( + interface, + "Failed to build path: no space at depth {depth}" ) .unwrap(); - - let value_hash: u64 = yafnv::fnv1a(value); - - let default_value = match defaults - .get_json_by_key(&path, interface.buffer) - { - Err(e) => { - writeln!( - interface, - "[default serialization error: {e}]" - ) - .unwrap(); - continue; - } - Ok(len) => { - core::str::from_utf8(&interface.buffer[..len]) - .unwrap() - } - }; - - let default_hash: u64 = yafnv::fnv1a(default_value); - if default_hash != value_hash { - writeln!( - interface.platform.interface_mut(), - " [default: {default_value}]" - ) - } else { - writeln!( - interface.platform.interface_mut(), - " [default]" - ) - } } } - .unwrap() } } - fn handle_clear( + fn handle_get( _menu: &menu::Menu, item: &menu::Item, args: &[&str], interface: &mut Self, settings: &mut P::Settings, ) { - let maybe_key = menu::argument_finder(item, args, "item").unwrap(); - if let Some(key) = maybe_key { - let mut defaults = settings.clone(); - defaults.reset(); - - let len = match defaults.get_json(key, interface.buffer) { - Err(e) => { - writeln!(interface, "Failed to clear `{key}`: {e:?}") + let key = menu::argument_finder(item, args, "path").unwrap(); + Self::iter_root( + key, + interface, + settings, + |key, interface, settings, defaults| { + // Get current + let check = match settings + .get_json_by_key(&key, interface.buffer) + { + Err(miniconf::Error::Traversal(Traversal::Absent(_))) => { + return; + } + Err(e) => { + writeln!(interface, "Failed to get `{}`: {e}", *key) + .unwrap(); + return; + } + Ok(len) => { + write!( + interface.platform.interface_mut(), + "{}: {}", + *key, + core::str::from_utf8(&interface.buffer[..len]) + .unwrap() + ) .unwrap(); - return; - } - - Ok(len) => len, - }; - - if let Err(e) = settings.set_json(key, &interface.buffer[..len]) { - writeln!(interface, "Failed to update {key}: {e:?}").unwrap(); - return; - } + yafnv::fnv1a::(&interface.buffer[..len]) + } + }; - interface.updated = true; - writeln!(interface, "{key} cleared to default").unwrap(); - } else { - settings.reset(); - interface.updated = true; - writeln!(interface, "All settings cleared").unwrap(); - } + // Get default and compare + match defaults.get_json_by_key(&key, interface.buffer) { + Err(miniconf::Error::Traversal(Traversal::Absent(_))) => { + write!(interface, " [default: absent]") + } + Err(e) => { + write!(interface, " [default serialization error: {e}]") + } + Ok(len) => { + if yafnv::fnv1a::(&interface.buffer[..len]) + != check + { + write!( + interface.platform.interface_mut(), + " [default: {}]", + core::str::from_utf8(&interface.buffer[..len]) + .unwrap() + ) + } else { + write!(interface, " [default]") + } + } + } + .unwrap(); - interface.platform.clear(interface.buffer, maybe_key); + // Get stored and compare + match interface.platform.fetch(interface.buffer, key.as_bytes()) + { + Err(e) => write!( + interface, + " [fetch error: {e:?}]" + ), + Ok(None) => + write!(interface, " [not stored]"), + Ok(Some(stored)) => { + let slic = postcard::de_flavors::Slice::new(stored); + // Use defaults as scratch space for postcard->json conversion + match defaults.set_postcard_by_key(&key, slic) { + Err(e) => write!( + interface, + " [stored deserialize error: {e}]" + ), + Ok(_rest) => + match defaults.get_json_by_key(&key, interface.buffer) { + Err(e) => write!( + interface, + " [stored serialization error: {e}]" + ), + Ok(len) => { + if yafnv::fnv1a::(&interface.buffer[..len]) != check { + write!( + interface.platform.interface_mut(), + " [stored: {}]", + core::str::from_utf8(&interface.buffer[..len]).unwrap()) + } else { + write!( + interface, + " [stored]" + ) + } + }, + } + } + } + }.unwrap(); + writeln!(interface).unwrap(); + }, + ); } - fn handle_get( + fn handle_clear( _menu: &menu::Menu, item: &menu::Item, args: &[&str], interface: &mut Self, settings: &mut P::Settings, ) { - let key = menu::argument_finder(item, args, "item").unwrap().unwrap(); - match settings.get_json(key, interface.buffer) { - Err(e) => { - writeln!(interface, "Failed to read {key}: {e}") - } - Ok(len) => { - writeln!( - interface.platform.interface_mut(), - "{key}: {}", - core::str::from_utf8(&interface.buffer[..len]).unwrap() - ) - } - } - .unwrap(); + let key = menu::argument_finder(item, args, "path").unwrap(); + Self::iter_root( + key, + interface, + settings, + |key, interface, settings, defaults| { + // Get current value checksum + let slic = postcard::ser_flavors::Slice::new(interface.buffer); + let check = match settings.get_postcard_by_key(&key, slic) { + Err(miniconf::Error::Traversal(Traversal::Absent(_))) => { + return; + } + Err(e) => { + writeln!(interface, "Failed to get {}: {e:?}", *key) + .unwrap(); + return; + } + Ok(slic) => yafnv::fnv1a::(slic), + }; + + // Get default if different + let slic = postcard::ser_flavors::Slice::new(interface.buffer); + let slic = match defaults.get_postcard_by_key(&key, slic) { + Err(miniconf::Error::Traversal(Traversal::Absent(_))) => { + log::warn!( + "Can't clear. Default is absent: `{}`", + *key + ); + None + } + Err(e) => { + writeln!( + interface, + "Failed to get default `{}`: {e}", + *key + ) + .unwrap(); + return; + } + Ok(slic) => { + if yafnv::fnv1a::(slic) != check { + Some(slic) + } else { + None + } + } + }; + + // Set default + if let Some(slic) = slic { + let slic = postcard::de_flavors::Slice::new(slic); + match settings.set_postcard_by_key(&key, slic) { + Err(miniconf::Error::Traversal(Traversal::Absent( + _, + ))) => { + return; + } + Err(e) => { + writeln!( + interface, + "Failed to set {}: {e:?}", + *key + ) + .unwrap(); + return; + } + Ok(_rest) => { + interface.updated = true; + writeln!(interface, "Cleared current `{}`", *key) + .unwrap() + } + } + } + + // Check for stored + match interface.platform.fetch(interface.buffer, key.as_bytes()) + { + Err(e) => { + writeln!( + interface, + "Failed to fetch `{}`: {e:?}", + *key + ) + .unwrap(); + } + Ok(None) => {} + // Clear stored + Ok(Some(_stored)) => match interface + .platform + .clear(interface.buffer, key.as_bytes()) + { + Ok(()) => { + writeln!(interface, "Clear stored `{}`", *key) + } + Err(e) => { + writeln!( + interface, + "Failed to clear `{}` from storage: {e:?}", + *key + ) + } + } + .unwrap(), + } + }, + ); + interface.updated = true; + writeln!(interface, "Some values may require reboot to become active") + .unwrap(); } - fn handle_save( + fn handle_store( _menu: &menu::Menu, item: &menu::Item, args: &[&str], interface: &mut Self, settings: &mut P::Settings, ) { - match interface.platform.save(interface.buffer, menu::argument_finder(item, args, "item").unwrap(), settings) { - Ok(_) => writeln!( - interface, - "Settings saved. You may need to reboot for the settings to be applied" - ) - .unwrap(), - Err(e) => { - writeln!(interface, "Failed to save settings: {e:?}").unwrap() - } - } + let key = menu::argument_finder(item, args, "path").unwrap(); + let force = menu::argument_finder(item, args, "force") + .unwrap() + .is_some(); + Self::iter_root( + key, + interface, + settings, + |key, interface, settings, defaults| { + // Get default value checksum + let slic = postcard::ser_flavors::Slice::new(interface.buffer); + let mut check = match defaults.get_postcard_by_key(&key, slic) { + // Could also serialize directly into the hasher for all these checksum calcs + Ok(slic) => yafnv::fnv1a::(slic), + Err(miniconf::Error::Traversal(Traversal::Absent( + _depth, + ))) => { + log::warn!("Default absent: `{}`", *key); + return; + } + Err(e) => { + writeln!( + interface, + "Failed to get `{}` default: {e:?}", + *key + ) + .unwrap(); + return; + } + }; + + // Get stored value checksum + match interface.platform.fetch(interface.buffer, key.as_bytes()) + { + Ok(None) => {} + Ok(Some(stored)) => { + let stored = yafnv::fnv1a::(stored); + if stored != check { + log::debug!( + "Stored differs from default: `{}`", + *key + ); + } else { + log::debug!("Stored matches default: `{}`", *key); + } + check = stored; + } + Err(e) => { + writeln!( + interface, + "Failed to fetch `{}`: {e:?}", + *key + ) + .unwrap(); + } + } + + // Get value + let slic = postcard::ser_flavors::Slice::new(interface.buffer); + let value = match settings.get_postcard_by_key(&key, slic) { + Ok(value) => value, + Err(miniconf::Error::Traversal(Traversal::Absent( + _depth, + ))) => { + return; + } + Err(e) => { + writeln!(interface, "Could not get `{}`: {e}", *key) + .unwrap(); + return; + } + }; + + // Check for mismatch + if yafnv::fnv1a::(value) == check && !force { + log::debug!( + "Not saving matching default/stored `{}`", + *key + ); + return; + } + let len = value.len(); + let (value, rest) = interface.buffer.split_at_mut(len); + + // Store + match interface.platform.store(rest, key.as_bytes(), value) { + Ok(_) => writeln!(interface, "`{}` stored", *key), + Err(e) => { + writeln!(interface, "Failed to store `{}`: {e:?}", *key) + } + } + .unwrap(); + }, + ); + writeln!(interface, "Some values may require reboot to become active") + .unwrap(); } fn handle_set( @@ -288,88 +511,85 @@ impl<'a, P: Platform, const Y: usize> Interface<'a, P, Y> { interface: &mut Self, settings: &mut P::Settings, ) { - let key = menu::argument_finder(item, args, "item").unwrap().unwrap(); + let key = menu::argument_finder(item, args, "path").unwrap().unwrap(); let value = menu::argument_finder(item, args, "value").unwrap().unwrap(); // Now, write the new value into memory. - match settings.set_json(key, value.as_bytes()) - { + match settings.set_json(key, value.as_bytes()) { Ok(_) => { interface.updated = true; writeln!( - interface, - "Settings updated. You may need to reboot for the setting to be applied" - ) - }, + interface, + "Set but not stored. May require store and reboot to activate." + ) + } Err(e) => { - writeln!(interface, "Failed to update {key}: {e:?}") + writeln!(interface, "Failed to set `{key}`: {e:?}") } - }.unwrap(); + } + .unwrap(); } fn menu() -> menu::Menu<'a, Self, P::Settings> { menu::Menu { label: "settings", items: &[ - &menu::Item { - command: "list", - help: Some("List all available settings and their current values."), - item_type: menu::ItemType::Callback { - function: Self::handle_list, - parameters: &[], - }, - }, &menu::Item { command: "get", - help: Some("Read a setting_from the device."), + help: Some("List paths and read current, default, and stored values"), item_type: menu::ItemType::Callback { function: Self::handle_get, - parameters: &[menu::Parameter::Mandatory { - parameter_name: "item", - help: Some("The name of the setting to read."), + parameters: &[menu::Parameter::Optional { + parameter_name: "path", + help: Some("The path of the value or subtree to list/read."), }] }, }, &menu::Item { command: "set", - help: Some("Update a a setting in the device."), + help: Some("Update a value"), item_type: menu::ItemType::Callback { function: Self::handle_set, parameters: &[ menu::Parameter::Mandatory { - parameter_name: "item", - help: Some("The name of the setting to write."), + parameter_name: "path", + help: Some("The path to set"), }, menu::Parameter::Mandatory { parameter_name: "value", - help: Some("Specifies the value to be written. Values must be JSON-encoded"), + help: Some("The value to be written, JSON-encoded"), }, ] }, }, &menu::Item { - command: "save", - help: Some("Save settings to the device."), + command: "store", + help: Some("Store values that differ from defaults"), item_type: menu::ItemType::Callback { - function: Self::handle_save, + function: Self::handle_store, parameters: &[ + menu::Parameter::Named { + parameter_name: "force", + help: Some("Also store values that match defaults"), + }, menu::Parameter::Optional { - parameter_name: "item", - help: Some("The name of the setting to clear."), + parameter_name: "path", + help: Some("The path of the value or subtree to store."), }, + ] }, }, &menu::Item { command: "clear", - help: Some("Clear the device settings to default values."), + help: Some("Clear active to defaults and remove all stored values"), item_type: menu::ItemType::Callback { function: Self::handle_clear, parameters: &[ menu::Parameter::Optional { - parameter_name: "item", - help: Some("The name of the setting to clear."), + parameter_name: "path", + help: Some("The path of the value or subtree to clear"), }, ] }, @@ -417,7 +637,7 @@ impl<'a, P: Platform, const Y: usize> Write for Interface<'a, P, Y> { } } -/// The serial settings management object. +// The Menu runner pub struct Runner<'a, P: Platform, const Y: usize>( menu::Runner<'a, Interface<'a, P, Y>, P::Settings>, ); @@ -431,7 +651,7 @@ impl<'a, P: Platform, const Y: usize> Runner<'a, P, Y> { /// * `line_buf` - A buffer used for maintaining the serial menu input line. It should be at /// least as long as the longest user input. /// * `serialize_buf` - A buffer used for serializing and deserializing settings. This buffer - /// needs to be at least as big as the entire serialized settings structure. + /// needs to be at least as big as twice the biggest serialized setting plus its path. pub fn new( platform: P, line_buf: &'a mut [u8], @@ -467,7 +687,7 @@ impl<'a, P: Platform, const Y: usize> Runner<'a, P, Y> { /// /// # Returns /// A boolean indicating true if the settings were modified. - pub fn process( + pub fn poll( &mut self, settings: &mut P::Settings, ) -> Result::Error> { diff --git a/src/bin/dual-iir.rs b/src/bin/dual-iir.rs index c5724a8dd..5dc1c7a8e 100644 --- a/src/bin/dual-iir.rs +++ b/src/bin/dual-iir.rs @@ -529,7 +529,7 @@ mod app { }); c.shared.settings.lock(|settings| { - if c.local.usb_terminal.process(settings).unwrap() { + if c.local.usb_terminal.poll(settings).unwrap() { settings_update::spawn().unwrap() } }); diff --git a/src/bin/lockin.rs b/src/bin/lockin.rs index f317a2116..77924c843 100644 --- a/src/bin/lockin.rs +++ b/src/bin/lockin.rs @@ -573,7 +573,7 @@ mod app { }); c.shared.settings.lock(|settings| { - if c.local.usb_terminal.process(settings).unwrap() { + if c.local.usb_terminal.poll(settings).unwrap() { settings_update::spawn().unwrap() } }); diff --git a/src/hardware/setup.rs b/src/hardware/setup.rs index b7323d30d..aa9d07ddd 100644 --- a/src/hardware/setup.rs +++ b/src/hardware/setup.rs @@ -644,7 +644,10 @@ where }; let mut settings = C::new(NetSettings::new(mac_addr)); - crate::settings::load_from_flash(&mut settings, &mut flash); + crate::settings::SerialSettingsPlatform::load( + &mut settings, + &mut flash, + ); (flash, settings) }; diff --git a/src/settings.rs b/src/settings.rs index 6e48cbacd..919ac85d4 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -75,56 +75,10 @@ pub trait AppSettings { fn net(&self) -> &NetSettings; } -pub fn load_from_flash JsonCoreSlash<'d, Y>, const Y: usize>( - structure: &mut T, - storage: &mut Flash, -) { - // Loop over flash and read settings - let mut buffer = [0u8; 512]; - for path in T::nodes::, '/'>>() { - let (path, _node) = path.unwrap(); - - // Try to fetch the setting from flash. - let item: SettingsItem = match block_on(fetch_item( - storage, - storage.range(), - &mut NoCache::new(), - &mut buffer, - SettingsKey(path.clone()), - )) { - Err(e) => { - log::warn!( - "Failed to fetch `{}` from flash: {e:?}", - path.as_str() - ); - continue; - } - Ok(Some(item)) => item, - _ => continue, - }; - - // An empty vector may be saved to flash to "erase" a setting, since the H7 doesn't support - // multi-write NOR flash. If we see an empty vector, ignore this entry. - if item.0.is_empty() { - continue; - } - - log::info!("Loading initial `{}` from flash", path.as_str()); - - let flavor = postcard::de_flavors::Slice::new(&item.0); - if let Err(e) = structure.set_postcard_by_key(&path, flavor) { - log::warn!( - "Failed to deserialize `{}` from flash: {e:?}", - path.as_str() - ); - } - } -} - #[derive( Default, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq, )] -pub struct SettingsKey(Path, '/'>); +pub struct SettingsKey(Vec); impl sequential_storage::map::Key for SettingsKey { fn serialize_into( @@ -146,43 +100,6 @@ impl sequential_storage::map::Key for SettingsKey { } } -#[derive( - Default, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq, -)] -pub struct SettingsItem(Vec); - -impl<'a> sequential_storage::map::Value<'a> for SettingsItem { - fn serialize_into( - &self, - buffer: &mut [u8], - ) -> Result { - if let Some(buf) = buffer.get_mut(..self.0.len()) { - buf.copy_from_slice(&self.0); - Ok(self.0.len()) - } else { - Err(SerializationError::BufferTooSmall) - } - } - - fn deserialize_from(buffer: &'a [u8]) -> Result { - Vec::from_slice(buffer) - .map_err(|_| SerializationError::BufferTooSmall) - .map(Self) - } -} - -#[derive(Debug)] -pub enum Error { - Postcard(postcard::Error), - Flash(F), -} - -impl From for Error { - fn from(e: postcard::Error) -> Self { - Self::Postcard(e) - } -} - pub struct SerialSettingsPlatform { /// The interface to read/write data to/from serially (via text) to the user. pub interface: BestEffortInterface, @@ -196,80 +113,99 @@ pub struct SerialSettingsPlatform { pub metadata: &'static ApplicationMetadata, } -impl Platform for SerialSettingsPlatform +impl SerialSettingsPlatform where - C: Settings, + C: for<'d> JsonCoreSlash<'d, Y>, { - type Interface = BestEffortInterface; - type Settings = C; - type Error = Error< - ::Error, - >; - - fn save( - &mut self, - buf: &mut [u8], - key: Option<&str>, - settings: &Self::Settings, - ) -> Result<(), Self::Error> { - let mut save_setting = |path| { - let path = SettingsKey(path); - - let mut data = Vec::new(); - data.resize(data.capacity(), 0).unwrap(); - let flavor = postcard::ser_flavors::Slice::new(&mut data); - - let len = match settings.get_postcard_by_key(&path.0, flavor) { + pub fn load(structure: &mut C, storage: &mut Flash) { + // Loop over flash and read settings + let mut buffer = [0u8; 512]; + for path in C::nodes::, '/'>>() { + let (path, _node) = path.unwrap(); + + // Try to fetch the setting from flash. + let value: &[u8] = match block_on(fetch_item( + storage, + storage.range(), + &mut NoCache::new(), + &mut buffer, + SettingsKey(Vec::try_from(path.as_bytes()).unwrap()), + )) { Err(e) => { log::warn!( - "Failed to save `{}` to flash: {e:?}", - path.0.as_str() + "Failed to fetch `{}` from flash: {e:?}", + path.as_str() ); - return Ok::<_, Self::Error>(()); + continue; } - Ok(slice) => slice.len(), + Ok(Some(value)) => value, + Ok(None) => continue, }; - data.truncate(len); - - let range = self.storage.range(); - // Check if the settings has changed from what's currently in flash (or if it doesn't - // yet exist). - if block_on(fetch_item( - &mut self.storage, - range.clone(), - &mut NoCache::new(), - buf, - path.clone(), - )) - .unwrap() - .map(|old: SettingsItem| old.0 != data) - .unwrap_or(true) - { - log::info!("Storing `{}` to flash", path.0.as_str()); - block_on(store_item( - &mut self.storage, - range, - &mut NoCache::new(), - buf, - path, - &SettingsItem(data), - )) - .unwrap(); + // An empty vector may be saved to flash to "erase" a setting, since the H7 doesn't support + // multi-write NOR flash. If we see an empty vector, ignore this entry. + if value.is_empty() { + continue; } - Ok(()) - }; + log::info!("Loading initial `{}` from flash", path.as_str()); - if let Some(key) = key { - save_setting(String::try_from(key).unwrap().into())?; - } else { - for path in Self::Settings::nodes() { - save_setting(path.unwrap().0)?; + let flavor = postcard::de_flavors::Slice::new(value); + if let Err(e) = structure.set_postcard_by_key(&path, flavor) { + log::warn!( + "Failed to deserialize `{}` from flash: {e:?}", + path.as_str() + ); } } + } +} + +impl Platform for SerialSettingsPlatform +where + C: Settings, +{ + type Interface = BestEffortInterface; + type Settings = C; + type Error = sequential_storage::Error< + ::Error, + >; - Ok(()) + fn fetch<'a>( + &mut self, + buf: &'a mut [u8], + key: &[u8], + ) -> Result, Self::Error> { + let range = self.storage.range(); + block_on(fetch_item( + &mut self.storage, + range, + &mut NoCache::new(), + buf, + SettingsKey(Vec::try_from(key).unwrap()), + )) + .map(|v| v.filter(|v: &&[u8]| !v.is_empty())) + } + + fn store( + &mut self, + buf: &mut [u8], + key: &[u8], + value: &[u8], + ) -> Result<(), Self::Error> { + let range = self.storage.range(); + block_on(store_item( + &mut self.storage, + range, + &mut NoCache::new(), + buf, + SettingsKey(Vec::try_from(key).unwrap()), + &value, + )) + } + + fn clear(&mut self, buf: &mut [u8], key: &[u8]) -> Result<(), Self::Error> { + self.store(buf, key, b"") } fn cmd(&mut self, cmd: &str) { @@ -323,50 +259,4 @@ where fn interface_mut(&mut self) -> &mut Self::Interface { &mut self.interface } - - fn clear(&mut self, buf: &mut [u8], key: Option<&str>) { - let mut erase_setting = |path| { - let path = SettingsKey(path); - let range = self.storage.range(); - - // Check if there's an entry for this item in our flash map. The item might be a - // sentinel value indicating "erased". Because we can't write flash memory twice, we - // instead append a sentry "erased" value to the map where the serialized value is - // empty. - let maybe_item: Option = block_on(fetch_item( - &mut self.storage, - range.clone(), - &mut NoCache::new(), - buf, - path.clone(), - )) - .unwrap(); - - // An entry may exist in the map with no data as a sentinel that this path was - // previously erased. If we find this, there's no need to store a duplicate "item is - // erased" sentinel in flash. We only need to logically erase the path from the map if - // it existed there in the first place. - if matches!(maybe_item, Some(item) if !item.0.is_empty()) { - block_on(store_item( - &mut self.storage, - range, - &mut NoCache::new(), - buf, - path, - &SettingsItem(Vec::new()), - )) - .unwrap(); - } - - Ok::<_, Self::Error>(()) - }; - - if let Some(key) = key { - erase_setting(String::try_from(key).unwrap().into()).unwrap(); - } else { - for path in Self::Settings::nodes() { - erase_setting(path.unwrap().0).unwrap(); - } - } - } }