stabilizer/
settings.rs

1//! Stabilizer Settings Management
2//!
3//! # Design
4//! Stabilizer supports two types of settings:
5//! 1. Static Device Configuration
6//! 2. Dynamic Run-time Settings
7//!
8//! Static device configuration settings are loaded and used only at device power-up. These include
9//! things like the MQTT broker address and the MQTT identifier. Conversely, the dynamic run-time
10//! settings can be changed and take effect immediately during device operation.
11//!
12//! This settings management interface is currently targeted at the static device configuration
13//! settings. Settings are persisted into the unused 1MB flash bank of Stabilizer for future
14//! recall. They can be modified via the USB interface to facilitate device configuration.
15//!
16//! Settings are stored in flash using a key-value pair mapping, where the `key` is the name of the
17//! entry in the settings structure. This has a number of benefits:
18//! 1. The `Settings` structure can have new entries added to it in the future without losing old
19//!    settings values, as each entry of the `Settings` struct is stored separately as its own
20//!    key-value pair.
21//! 2. The `Settings` can be used among multiple Stabilizer firmware versions that need the same
22//!    settings values
23//! 3. Unknown/unneeded settings values in flash can be actively ignored, facilitating simple flash
24//!    storage sharing.
25use crate::hardware::{flash::Flash, metadata::ApplicationMetadata, platform};
26use core::fmt::Write;
27use embassy_futures::block_on;
28use embedded_io::Write as EioWrite;
29use heapless::{String, Vec};
30use miniconf::{
31    postcard, Leaf, Path, Tree, TreeDeserializeOwned, TreeKey, TreeSerialize,
32};
33use sequential_storage::{
34    cache::NoCache,
35    map::{fetch_item, store_item, SerializationError},
36};
37use serial_settings::{BestEffortInterface, Platform, Settings};
38use smoltcp_nal::smoltcp::wire::EthernetAddress;
39use stm32h7xx_hal::flash::LockedFlashBank;
40
41/// Settings that are used for configuring the network interface to Stabilizer.
42#[derive(Clone, Debug, Tree)]
43pub struct NetSettings {
44    /// The broker domain name (or IP address) to use for MQTT connections.
45    pub broker: Leaf<String<255>>,
46
47    /// The MQTT ID to use upon connection with a broker.
48    pub id: Leaf<String<23>>,
49
50    /// An optional static IP address to use. An unspecified IP address (or malformed address) will
51    /// use DHCP.
52    pub ip: Leaf<String<15>>,
53    #[tree(skip)]
54    /// The MAC address of Stabilizer, which is used to reinitialize the ID to default settings.
55    pub mac: EthernetAddress,
56}
57
58impl NetSettings {
59    pub fn new(mac: EthernetAddress) -> Self {
60        let mut id = String::new();
61        write!(&mut id, "{mac}").unwrap();
62
63        Self {
64            broker: String::try_from("mqtt").unwrap().into(),
65            ip: String::try_from("0.0.0.0").unwrap().into(),
66            id: id.into(),
67            mac,
68        }
69    }
70}
71
72pub trait AppSettings {
73    /// Construct the settings given known network settings.
74    fn new(net: NetSettings) -> Self;
75
76    /// Get the network settings from the application settings.
77    fn net(&self) -> &NetSettings;
78}
79
80#[derive(
81    Default, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq,
82)]
83pub struct SettingsKey(Vec<u8, 128>);
84
85impl sequential_storage::map::Key for SettingsKey {
86    fn serialize_into(
87        &self,
88        buffer: &mut [u8],
89    ) -> Result<usize, SerializationError> {
90        Ok(::postcard::to_slice(self, buffer)
91            .map_err(|_| SerializationError::BufferTooSmall)?
92            .len())
93    }
94
95    fn deserialize_from(
96        buffer: &[u8],
97    ) -> Result<(Self, usize), SerializationError> {
98        let original_length = buffer.len();
99        let (result, remainder) = ::postcard::take_from_bytes(buffer)
100            .map_err(|_| SerializationError::BufferTooSmall)?;
101        Ok((result, original_length - remainder.len()))
102    }
103}
104
105pub struct SerialSettingsPlatform<C> {
106    /// The interface to read/write data to/from serially (via text) to the user.
107    pub interface: BestEffortInterface<crate::hardware::SerialPort>,
108
109    pub _settings_marker: core::marker::PhantomData<C>,
110
111    /// The storage mechanism used to persist settings to between boots.
112    pub storage: Flash,
113
114    /// Metadata associated with the application
115    pub metadata: &'static ApplicationMetadata,
116}
117
118impl<C> SerialSettingsPlatform<C>
119where
120    C: TreeDeserializeOwned + TreeSerialize + TreeKey,
121{
122    pub fn load(structure: &mut C, storage: &mut Flash) {
123        // Loop over flash and read settings
124        let mut buffer = [0u8; 512];
125        for path in C::nodes::<Path<String<128>, '/'>, 8>() {
126            let (path, _node) = path.unwrap();
127
128            // Try to fetch the setting from flash.
129            let value: &[u8] = match block_on(fetch_item(
130                storage,
131                storage.range(),
132                &mut NoCache::new(),
133                &mut buffer,
134                &SettingsKey(path.clone().into_inner().into_bytes()),
135            )) {
136                Err(e) => {
137                    log::warn!(
138                        "Failed to fetch `{}` from flash: {e:?}",
139                        path.as_str()
140                    );
141                    continue;
142                }
143                Ok(Some(value)) => value,
144                Ok(None) => continue,
145            };
146
147            // An empty vector may be saved to flash to "erase" a setting, since the H7 doesn't support
148            // multi-write NOR flash. If we see an empty vector, ignore this entry.
149            if value.is_empty() {
150                continue;
151            }
152
153            log::info!("Loading initial `{}` from flash", path.as_str());
154
155            let flavor = ::postcard::de_flavors::Slice::new(value);
156            if let Err(e) = postcard::set_by_key(structure, &path, flavor) {
157                log::warn!(
158                    "Failed to deserialize `{}` from flash: {e:?}",
159                    path.as_str()
160                );
161            }
162        }
163    }
164}
165
166impl<C> Platform for SerialSettingsPlatform<C>
167where
168    C: Settings,
169{
170    type Interface = BestEffortInterface<crate::hardware::SerialPort>;
171    type Settings = C;
172    type Error = sequential_storage::Error<
173        <LockedFlashBank as embedded_storage::nor_flash::ErrorType>::Error,
174    >;
175
176    fn fetch<'a>(
177        &mut self,
178        buf: &'a mut [u8],
179        key: &[u8],
180    ) -> Result<Option<&'a [u8]>, Self::Error> {
181        let range = self.storage.range();
182        block_on(fetch_item(
183            &mut self.storage,
184            range,
185            &mut NoCache::new(),
186            buf,
187            &SettingsKey(Vec::try_from(key).unwrap()),
188        ))
189        .map(|v| v.filter(|v: &&[u8]| !v.is_empty()))
190    }
191
192    fn store(
193        &mut self,
194        buf: &mut [u8],
195        key: &[u8],
196        value: &[u8],
197    ) -> Result<(), Self::Error> {
198        let range = self.storage.range();
199        block_on(store_item(
200            &mut self.storage,
201            range,
202            &mut NoCache::new(),
203            buf,
204            &SettingsKey(Vec::try_from(key).unwrap()),
205            &value,
206        ))
207    }
208
209    fn clear(&mut self, buf: &mut [u8], key: &[u8]) -> Result<(), Self::Error> {
210        self.store(buf, key, b"")
211    }
212
213    fn cmd(&mut self, cmd: &str) {
214        match cmd {
215            "reboot" => cortex_m::peripheral::SCB::sys_reset(),
216            "dfu" => platform::start_dfu_reboot(),
217            "service" => {
218                writeln!(
219                    &mut self.interface,
220                    "{:<20}: {} [{}]",
221                    "Version",
222                    self.metadata.firmware_version,
223                    self.metadata.profile,
224                )
225                .unwrap();
226                writeln!(
227                    &mut self.interface,
228                    "{:<20}: {}",
229                    "Hardware Revision", self.metadata.hardware_version
230                )
231                .unwrap();
232                writeln!(
233                    &mut self.interface,
234                    "{:<20}: {}",
235                    "Rustc Version", self.metadata.rust_version
236                )
237                .unwrap();
238                writeln!(
239                    &mut self.interface,
240                    "{:<20}: {}",
241                    "Features", self.metadata.features
242                )
243                .unwrap();
244                writeln!(
245                    &mut self.interface,
246                    "{:<20}: {}",
247                    "Panic Info", self.metadata.panic_info
248                )
249                .unwrap();
250            }
251            _ => {
252                writeln!(
253                    self.interface_mut(),
254                    "Invalid platform command: `{cmd}` not in [`dfu`, `reboot`, `service`]"
255                )
256                .ok();
257            }
258        }
259    }
260
261    fn interface_mut(&mut self) -> &mut Self::Interface {
262        &mut self.interface
263    }
264}