umsh_flash_store/
lib.rs

1//! Chip-agnostic implementation of the `umsh-hal` storage traits on top
2//! of [`sequential-storage`](https://docs.rs/sequential-storage).
3//!
4//! This is the storage engine every UMSH board shares. It is generic over
5//! the async flash driver (`F: MultiwriteNorFlash`) and the sharing mutex
6//! (`M: RawMutex`); chip BSPs supply concrete backings and alias the
7//! types.
8//!
9//! The same map serves every trait. Logical separation is by ASCII key
10//! prefix decided at the call site:
11//!
12//! | Prefix       | Trait           | Payload                              |
13//! |--------------|-----------------|--------------------------------------|
14//! | `id.sk`      | (direct)        | local Ed25519 secret scalar (32 B)   |
15//! | `peers`      | PeerStore       | packed pubkey index (32 B × N)       |
16//! | `peer:<pk>`  | PeerStore       | alias len (1 B) + alias (≤16 B)      |
17//! | `ch:<id>`    | KeyValueStore   | channel name + key + flags           |
18//! | `mac.tx:<pk>`| CounterStore    | TX reservation boundary (u32 LE)     |
19//! | `mac.rx:<pk>`| CounterStore    | RX replay-window boundary (u32 LE)   |
20//!
21//! ## CPU stall warning
22//!
23//! Page erases block the executor for a long time on every backend we
24//! use — the nRF52840 NVMC halts the CPU for ~85 ms, and the ESP32
25//! suspends the flash cache for the duration of the write. No amount of
26//! async scheduling can preempt either. Callers MUST batch writes; the
27//! MAC's TX-side `COUNTER_PERSIST_BLOCK_SIZE = 128` and the RX-side
28//! mirror keep this manageable for counters, and peer-record writes
29//! should be debounced at the application layer.
30//!
31//! ## Sharing model
32//!
33//! [`FlashStore`] owns the flash + map behind an async mutex.
34//! [`KeyValueView`], [`CounterView`], [`PeerView`], and [`ChannelView`]
35//! are zero-cost view types that each hold a `&'static FlashStore`. They
36//! exist as separate types because the `umsh-hal` traits both define
37//! `load` and `store` methods — implementing both on a single type would
38//! force every caller to disambiguate via UFCS. Keep them split.
39#![no_std]
40
41use core::ops::Range;
42
43use embassy_sync::blocking_mutex::raw::RawMutex;
44use embassy_sync::mutex::Mutex;
45use embedded_storage_async::nor_flash::MultiwriteNorFlash;
46use heapless::Vec;
47use sequential_storage::cache::NoCache;
48use sequential_storage::map::{MapConfig, MapStorage};
49
50/// Maximum stored key length. Covers an 8-byte ASCII prefix plus a
51/// 32-byte Ed25519 pubkey with headroom for shorter prefixes / new
52/// namespaces.
53pub const MAX_KEY_LEN: usize = 64;
54
55/// Per-call scratch buffer size used for sequential-storage's serialise
56/// / deserialise workspace. Must hold the largest (serialised key +
57/// value) pair the store ever sees. 512 B comfortably covers a 64 B
58/// key plus a ~256 B peer record.
59pub const SCRATCH_LEN: usize = 512;
60
61/// Heapless `Vec` used as the in-memory key representation, with the
62/// `sequential-storage` `Key` impl provided by the `heapless` feature.
63type StoreKey = Vec<u8, MAX_KEY_LEN>;
64
65/// Maximum number of peers tracked in the peer index.
66/// 8 × 32 = 256 bytes, comfortably under the 512-byte scratch limit.
67pub const MAX_PEERS: usize = 8;
68
69/// Maximum alias length in bytes (UTF-8).
70pub const MAX_ALIAS_LEN: usize = 16;
71
72/// Fixed-size alias header prepended to every peer record.
73///
74/// Layout:
75/// ```text
76/// Byte 0      alias_len  (0 = no alias, 1–16 = alias present)
77/// Bytes 1–16  alias data (16-byte slot; only alias_len bytes significant)
78/// ```
79pub const ALIAS_HEADER_LEN: usize = 1 + MAX_ALIAS_LEN; // 17 bytes
80
81/// Maximum total peer record size.
82///
83/// `ALIAS_HEADER_LEN` (17) + serialised `NodeIdentityPayload` (≤239 B).
84/// Full NodeIdentityPayload with all optional fields is well under 150 B,
85/// so 256 B provides ample headroom for future additions.
86pub const MAX_PEER_RECORD_LEN: usize = 256;
87
88/// Errors surfaced by this module, generic over the flash driver's own
89/// error type.
90#[derive(Debug)]
91pub enum Error<E> {
92    /// Caller-supplied key exceeded [`MAX_KEY_LEN`].
93    KeyTooLong,
94    /// Stored value did not fit in the caller-supplied buffer on load.
95    ValueTooLong,
96    /// The peer index already holds [`MAX_PEERS`] entries.
97    PeerIndexFull,
98    /// A stored record contained unexpected bytes (e.g. invalid UTF-8 alias).
99    CorruptedData,
100    /// `sequential-storage` returned an error (corruption, full storage,
101    /// underlying flash failure, …).
102    Storage(sequential_storage::Error<E>),
103}
104
105impl<E> From<sequential_storage::Error<E>> for Error<E> {
106    fn from(err: sequential_storage::Error<E>) -> Self {
107        Self::Storage(err)
108    }
109}
110
111/// Owns the flash driver and the `sequential-storage` map.
112///
113/// Construct once during board init, place in a `StaticCell`, then hand
114/// `&'static` references to the view constructors when building the
115/// `Mac` platform.
116pub struct FlashStore<F, M>
117where
118    F: MultiwriteNorFlash,
119    M: RawMutex,
120{
121    map: Mutex<M, MapStorage<StoreKey, F, NoCache>>,
122}
123
124impl<F, M> FlashStore<F, M>
125where
126    F: MultiwriteNorFlash,
127    M: RawMutex,
128{
129    /// Wrap an async flash driver over `range`. Does NOT erase or format
130    /// the flash — the underlying map mounts lazily on first access.
131    ///
132    /// `range` must be page-aligned and at least two pages long;
133    /// `MapConfig::new` panics otherwise. Callers pass a board constant,
134    /// so a bad geometry is a boot-time panic rather than a silent
135    /// corruption.
136    pub fn new(flash: F, range: Range<u32>) -> Self {
137        let cfg = MapConfig::new(range);
138        Self {
139            map: Mutex::new(MapStorage::new(flash, cfg, NoCache::new())),
140        }
141    }
142
143    async fn load_bytes(
144        &self,
145        key: &[u8],
146        out: &mut [u8],
147    ) -> Result<Option<usize>, Error<F::Error>> {
148        let store_key = make_key(key)?;
149        let mut scratch = [0u8; SCRATCH_LEN];
150        let mut guard = self.map.lock().await;
151        let result: Option<&[u8]> = guard.fetch_item(&mut scratch, &store_key).await?;
152        match result {
153            None => Ok(None),
154            Some(bytes) => {
155                if bytes.len() > out.len() {
156                    return Err(Error::ValueTooLong);
157                }
158                out[..bytes.len()].copy_from_slice(bytes);
159                Ok(Some(bytes.len()))
160            }
161        }
162    }
163
164    async fn store_bytes(&self, key: &[u8], value: &[u8]) -> Result<(), Error<F::Error>> {
165        let store_key = make_key(key)?;
166        let mut scratch = [0u8; SCRATCH_LEN];
167        let mut guard = self.map.lock().await;
168        guard.store_item(&mut scratch, &store_key, &value).await?;
169        Ok(())
170    }
171
172    async fn delete_bytes(&self, key: &[u8]) -> Result<(), Error<F::Error>> {
173        let store_key = make_key(key)?;
174        let mut scratch = [0u8; SCRATCH_LEN];
175        let mut guard = self.map.lock().await;
176        guard.remove_item(&mut scratch, &store_key).await?;
177        Ok(())
178    }
179}
180
181fn make_key<E>(bytes: &[u8]) -> Result<StoreKey, Error<E>> {
182    StoreKey::from_slice(bytes).map_err(|_| Error::KeyTooLong)
183}
184
185// ─── Identity helpers ─────────────────────────────────────────────────────────
186
187/// Key under which the local Ed25519 secret scalar is stored.
188const SK_KEY: &[u8] = b"id.sk";
189const TRACKER_PREFERENCES_KEY: &[u8] = b"ux.tracker";
190
191impl<F, M> FlashStore<F, M>
192where
193    F: MultiwriteNorFlash,
194    M: RawMutex,
195{
196    /// Load the board-independent encoded tracker preferences byte.
197    pub async fn load_tracker_preferences(&self) -> Result<Option<u8>, Error<F::Error>> {
198        let mut byte = [0u8; 1];
199        match self.load_bytes(TRACKER_PREFERENCES_KEY, &mut byte).await? {
200            Some(1) => Ok(Some(byte[0])),
201            Some(_) => Err(Error::CorruptedData),
202            None => Ok(None),
203        }
204    }
205
206    /// Persist the board-independent encoded tracker preferences byte.
207    pub async fn store_tracker_preferences(&self, value: u8) -> Result<(), Error<F::Error>> {
208        self.store_bytes(TRACKER_PREFERENCES_KEY, &[value]).await
209    }
210
211    /// Load the local Ed25519 secret key from storage.
212    ///
213    /// Returns `Ok(Some(sk))` when a valid 32-byte key is present,
214    /// `Ok(None)` when no key has been stored yet (first boot), and
215    /// `Err` on a storage or hardware failure.
216    pub async fn load_sk(&self) -> Result<Option<[u8; 32]>, Error<F::Error>> {
217        let mut buf = [0u8; 32];
218        match self.load_bytes(SK_KEY, &mut buf).await? {
219            Some(32) => Ok(Some(buf)),
220            // Missing or wrong length — treat as "not yet written".
221            Some(_) | None => Ok(None),
222        }
223    }
224
225    /// Persist the local Ed25519 secret key.
226    ///
227    /// Call this exactly once on first boot, after generating the key
228    /// from the hardware TRNG. Subsequent boots should use [`load_sk`].
229    ///
230    /// [`load_sk`]: Self::load_sk
231    pub async fn store_sk(&self, sk: &[u8; 32]) -> Result<(), Error<F::Error>> {
232        self.store_bytes(SK_KEY, sk).await
233    }
234}
235
236// ─── Peer storage helpers ─────────────────────────────────────────────────────
237
238/// Key under which the packed peer index is stored (`peers`).
239const PEER_INDEX_KEY: &[u8] = b"peers";
240/// Prefix for individual peer records (`peer:` + 32-byte pubkey = 37 bytes).
241const PEER_KEY_PREFIX: &[u8] = b"peer:";
242
243fn make_peer_key<E>(pk: &[u8; 32]) -> Result<StoreKey, Error<E>> {
244    let mut key = StoreKey::new();
245    let r1 = key.extend_from_slice(PEER_KEY_PREFIX);
246    let r2 = key.extend_from_slice(pk);
247    if r1.is_err() || r2.is_err() {
248        return Err(Error::KeyTooLong);
249    }
250    Ok(key)
251}
252
253impl<F, M> FlashStore<F, M>
254where
255    F: MultiwriteNorFlash,
256    M: RawMutex,
257{
258    /// Load every persisted peer into `out`.
259    ///
260    /// Each entry is a raw 32-byte public key plus an optional alias string
261    /// (up to 16 UTF-8 bytes). Entries beyond `N` are silently dropped.
262    pub async fn load_all_peers<const N: usize>(
263        &self,
264        out: &mut Vec<([u8; 32], Option<heapless::String<MAX_ALIAS_LEN>>), N>,
265    ) -> Result<(), Error<F::Error>> {
266        let mut index_buf = [0u8; 32 * MAX_PEERS];
267        let n = match self.load_bytes(PEER_INDEX_KEY, &mut index_buf).await? {
268            None => return Ok(()),
269            Some(n) if n % 32 == 0 => n,
270            Some(_) => return Err(Error::CorruptedData),
271        };
272        for chunk in index_buf[..n].chunks_exact(32) {
273            let mut pk = [0u8; 32];
274            pk.copy_from_slice(chunk);
275            let alias = self.load_peer_alias(&pk).await?;
276            let _ = out.push((pk, alias));
277        }
278        Ok(())
279    }
280
281    /// Read the full raw peer record into a [`MAX_PEER_RECORD_LEN`]-byte buffer.
282    ///
283    /// Returns `(buf, len)` when present. The alias header occupies
284    /// `buf[0..ALIAS_HEADER_LEN]` and any identity bytes follow at
285    /// `buf[ALIAS_HEADER_LEN..len]`.
286    async fn read_peer_record(
287        &self,
288        pk: &[u8; 32],
289    ) -> Result<Option<([u8; MAX_PEER_RECORD_LEN], usize)>, Error<F::Error>> {
290        let key = make_peer_key(pk)?;
291        let mut buf = [0u8; MAX_PEER_RECORD_LEN];
292        match self.load_bytes(&key, &mut buf).await? {
293            None => Ok(None),
294            Some(n) => Ok(Some((buf, n))),
295        }
296    }
297
298    async fn load_peer_alias(
299        &self,
300        pk: &[u8; 32],
301    ) -> Result<Option<heapless::String<MAX_ALIAS_LEN>>, Error<F::Error>> {
302        let (buf, n) = match self.read_peer_record(pk).await? {
303            None => return Ok(None),
304            Some(x) => x,
305        };
306        if n < 1 {
307            return Ok(None);
308        }
309        let alias_len = buf[0] as usize;
310        if alias_len == 0 || n < 1 + alias_len {
311            return Ok(None);
312        }
313        let s = core::str::from_utf8(&buf[1..1 + alias_len]).map_err(|_| Error::CorruptedData)?;
314        Ok(heapless::String::try_from(s).ok())
315    }
316
317    /// Upsert the alias for `pk`.
318    ///
319    /// Writes the alias header and appends `pk` to the peer index if not
320    /// already present. Any previously stored identity bytes are preserved.
321    /// `alias`, if supplied, must be at most [`MAX_ALIAS_LEN`] bytes.
322    pub async fn store_peer_entry(
323        &self,
324        pk: &[u8; 32],
325        alias: Option<&[u8]>,
326    ) -> Result<(), Error<F::Error>> {
327        let key = make_peer_key(pk)?;
328
329        // Read existing record to preserve any identity bytes.
330        let (mut value, existing_len) = match self.read_peer_record(pk).await? {
331            Some((buf, n)) => (buf, n),
332            None => ([0u8; MAX_PEER_RECORD_LEN], ALIAS_HEADER_LEN),
333        };
334        let identity_end = existing_len.max(ALIAS_HEADER_LEN);
335
336        // Overwrite the alias header in-place.
337        value[0] = 0;
338        if let Some(a) = alias {
339            if a.len() > MAX_ALIAS_LEN {
340                return Err(Error::ValueTooLong);
341            }
342            value[0] = a.len() as u8;
343            value[1..1 + a.len()].copy_from_slice(a);
344            // Zero the padding in the alias slot so the record stays canonical.
345            value[1 + a.len()..ALIAS_HEADER_LEN].fill(0);
346        } else {
347            value[1..ALIAS_HEADER_LEN].fill(0);
348        }
349
350        self.store_bytes(&key, &value[..identity_end]).await?;
351
352        // Update peer index: load, add if missing, store.
353        let mut index_buf = [0u8; 32 * MAX_PEERS];
354        let existing_n = match self.load_bytes(PEER_INDEX_KEY, &mut index_buf).await? {
355            Some(n) => n,
356            None => 0,
357        };
358        let already_present = index_buf[..existing_n]
359            .chunks_exact(32)
360            .any(|c| c == pk.as_slice());
361        if !already_present {
362            let new_n = existing_n + 32;
363            if new_n > index_buf.len() {
364                return Err(Error::PeerIndexFull);
365            }
366            index_buf[existing_n..new_n].copy_from_slice(pk);
367            self.store_bytes(PEER_INDEX_KEY, &index_buf[..new_n])
368                .await?;
369        }
370        Ok(())
371    }
372
373    /// Update (or clear) the serialised `NodeIdentityPayload` for `pk`.
374    ///
375    /// The alias header is preserved. Pass an empty slice to remove the
376    /// identity portion while keeping the alias. The peer must already be in
377    /// the peer index (i.e. `store_peer_entry` called first).
378    pub async fn update_peer_identity(
379        &self,
380        pk: &[u8; 32],
381        identity_bytes: &[u8],
382    ) -> Result<(), Error<F::Error>> {
383        let new_len = ALIAS_HEADER_LEN + identity_bytes.len();
384        if new_len > MAX_PEER_RECORD_LEN {
385            return Err(Error::ValueTooLong);
386        }
387        let key = make_peer_key(pk)?;
388
389        // Read existing record to preserve alias header.
390        let (mut value, _) = match self.read_peer_record(pk).await? {
391            Some(x) => x,
392            None => ([0u8; MAX_PEER_RECORD_LEN], 0), // peer not yet stored
393        };
394
395        value[ALIAS_HEADER_LEN..new_len].copy_from_slice(identity_bytes);
396        self.store_bytes(&key, &value[..new_len]).await?;
397        Ok(())
398    }
399
400    /// Load the raw serialised identity bytes for `pk` into `out`.
401    ///
402    /// Returns the number of bytes written when an identity record is present,
403    /// `None` when no identity has been stored yet.
404    pub async fn load_peer_identity(
405        &self,
406        pk: &[u8; 32],
407        out: &mut [u8],
408    ) -> Result<Option<usize>, Error<F::Error>> {
409        let (buf, n) = match self.read_peer_record(pk).await? {
410            None => return Ok(None),
411            Some(x) => x,
412        };
413        if n <= ALIAS_HEADER_LEN {
414            return Ok(None);
415        }
416        let identity = &buf[ALIAS_HEADER_LEN..n];
417        if identity.len() > out.len() {
418            return Err(Error::ValueTooLong);
419        }
420        out[..identity.len()].copy_from_slice(identity);
421        Ok(Some(identity.len()))
422    }
423
424    /// Return `true` if `pk` appears in the peer index.
425    ///
426    /// Used to guard `update_peer_identity` so that identity bytes received
427    /// over the air are only stored for peers the user has explicitly added.
428    pub async fn peer_exists(&self, pk: &[u8; 32]) -> Result<bool, Error<F::Error>> {
429        let mut index_buf = [0u8; 32 * MAX_PEERS];
430        let n = match self.load_bytes(PEER_INDEX_KEY, &mut index_buf).await? {
431            Some(n) => n,
432            None => return Ok(false),
433        };
434        Ok(index_buf[..n].chunks_exact(32).any(|c| c == pk.as_slice()))
435    }
436
437    /// Remove the peer record for `pk` from both the per-key record and the
438    /// peer index. A no-op if the peer was not previously stored.
439    pub async fn delete_peer_entry(&self, pk: &[u8; 32]) -> Result<(), Error<F::Error>> {
440        // Best-effort delete of the individual record.
441        let key = make_peer_key(pk)?;
442        let _ = self.delete_bytes(&key).await;
443
444        // Remove from index.
445        let mut index_buf = [0u8; 32 * MAX_PEERS];
446        let n = match self.load_bytes(PEER_INDEX_KEY, &mut index_buf).await? {
447            Some(n) => n,
448            None => return Ok(()),
449        };
450        let mut new_buf = [0u8; 32 * MAX_PEERS];
451        let mut new_n = 0usize;
452        for chunk in index_buf[..n].chunks_exact(32) {
453            if chunk != pk.as_slice() {
454                new_buf[new_n..new_n + 32].copy_from_slice(chunk);
455                new_n += 32;
456            }
457        }
458        if new_n == 0 {
459            let _ = self.delete_bytes(PEER_INDEX_KEY).await;
460        } else {
461            self.store_bytes(PEER_INDEX_KEY, &new_buf[..new_n]).await?;
462        }
463        Ok(())
464    }
465}
466
467// ─── Channel storage helpers ──────────────────────────────────────────────────
468
469/// Key under which the packed channel index is stored.
470const CH_INDEX_KEY: &[u8] = b"channels";
471/// Prefix for individual channel records (`ch:` + name).
472const CH_KEY_PREFIX: &[u8] = b"ch:";
473/// Maximum channel name length in bytes (UTF-8). Matches CliSession's alias cap.
474pub const MAX_CHANNEL_NAME_LEN: usize = 16;
475/// Fixed slot size for one channel-name entry in the index:
476/// 1 byte length + 16 bytes name data.
477const CH_NAME_SLOT_LEN: usize = 1 + MAX_CHANNEL_NAME_LEN;
478/// Maximum number of channels tracked in the channel index.
479pub const MAX_CHANNELS: usize = 8;
480
481fn make_channel_key<E>(name: &[u8]) -> Result<StoreKey, Error<E>> {
482    if name.len() > MAX_CHANNEL_NAME_LEN {
483        return Err(Error::KeyTooLong);
484    }
485    let mut key = StoreKey::new();
486    let r1 = key.extend_from_slice(CH_KEY_PREFIX);
487    let r2 = key.extend_from_slice(name);
488    if r1.is_err() || r2.is_err() {
489        return Err(Error::KeyTooLong);
490    }
491    Ok(key)
492}
493
494impl<F, M> FlashStore<F, M>
495where
496    F: MultiwriteNorFlash,
497    M: RawMutex,
498{
499    /// Load every persisted channel into `out`.
500    ///
501    /// Each entry is a name string (up to 16 UTF-8 bytes) and a 32-byte key.
502    /// Entries beyond `N` are silently dropped.
503    pub async fn load_all_channels<const N: usize>(
504        &self,
505        out: &mut Vec<(heapless::String<MAX_CHANNEL_NAME_LEN>, [u8; 32]), N>,
506    ) -> Result<(), Error<F::Error>> {
507        let mut index_buf = [0u8; CH_NAME_SLOT_LEN * MAX_CHANNELS];
508        let n = match self.load_bytes(CH_INDEX_KEY, &mut index_buf).await? {
509            None => return Ok(()),
510            Some(n) if n % CH_NAME_SLOT_LEN == 0 => n,
511            Some(_) => return Err(Error::CorruptedData),
512        };
513        for slot in index_buf[..n].chunks_exact(CH_NAME_SLOT_LEN) {
514            let name_len = slot[0] as usize;
515            if name_len == 0 || name_len > MAX_CHANNEL_NAME_LEN {
516                continue;
517            }
518            let name_bytes = &slot[1..1 + name_len];
519            let name = match core::str::from_utf8(name_bytes) {
520                Ok(s) => match heapless::String::try_from(s) {
521                    Ok(h) => h,
522                    Err(_) => continue,
523                },
524                Err(_) => continue,
525            };
526            let key_bytes = match self.load_channel_key(name_bytes).await? {
527                Some(k) => k,
528                None => continue,
529            };
530            let _ = out.push((name, key_bytes));
531        }
532        Ok(())
533    }
534
535    async fn load_channel_key(&self, name: &[u8]) -> Result<Option<[u8; 32]>, Error<F::Error>> {
536        let key = make_channel_key(name)?;
537        let mut buf = [0u8; 32];
538        match self.load_bytes(&key, &mut buf).await? {
539            Some(32) => Ok(Some(buf)),
540            _ => Ok(None),
541        }
542    }
543
544    /// Upsert the channel record for `name`.
545    ///
546    /// Writes the 32-byte key into the per-name record and appends the name
547    /// to the channel index if not already present.
548    pub async fn store_channel_entry(
549        &self,
550        name: &[u8],
551        key: &[u8; 32],
552    ) -> Result<(), Error<F::Error>> {
553        if name.len() > MAX_CHANNEL_NAME_LEN {
554            return Err(Error::ValueTooLong);
555        }
556        // Write individual record.
557        let ch_key = make_channel_key(name)?;
558        self.store_bytes(&ch_key, key).await?;
559
560        // Update index: load, add slot if missing, store.
561        let mut index_buf = [0u8; CH_NAME_SLOT_LEN * MAX_CHANNELS];
562        let existing_n = match self.load_bytes(CH_INDEX_KEY, &mut index_buf).await? {
563            Some(n) => n,
564            None => 0,
565        };
566        let already_present = index_buf[..existing_n]
567            .chunks_exact(CH_NAME_SLOT_LEN)
568            .any(|slot| slot[0] as usize == name.len() && &slot[1..1 + name.len()] == name);
569        if !already_present {
570            let new_n = existing_n + CH_NAME_SLOT_LEN;
571            if new_n > index_buf.len() {
572                return Err(Error::PeerIndexFull);
573            }
574            index_buf[existing_n] = name.len() as u8;
575            index_buf[existing_n + 1..existing_n + 1 + name.len()].copy_from_slice(name);
576            // Zero remaining padding in the slot.
577            index_buf[existing_n + 1 + name.len()..new_n].fill(0);
578            self.store_bytes(CH_INDEX_KEY, &index_buf[..new_n]).await?;
579        }
580        Ok(())
581    }
582
583    /// Remove the channel record for `name` from both the per-name record and
584    /// the channel index. A no-op if the channel was not previously stored.
585    pub async fn delete_channel_entry(&self, name: &[u8]) -> Result<(), Error<F::Error>> {
586        // Best-effort delete of the individual record.
587        if let Ok(ch_key) = make_channel_key::<F::Error>(name) {
588            let _ = self.delete_bytes(&ch_key).await;
589        }
590
591        // Remove from index.
592        let mut index_buf = [0u8; CH_NAME_SLOT_LEN * MAX_CHANNELS];
593        let n = match self.load_bytes(CH_INDEX_KEY, &mut index_buf).await? {
594            Some(n) => n,
595            None => return Ok(()),
596        };
597        let mut new_buf = [0u8; CH_NAME_SLOT_LEN * MAX_CHANNELS];
598        let mut new_n = 0usize;
599        for slot in index_buf[..n].chunks_exact(CH_NAME_SLOT_LEN) {
600            let slot_name_len = slot[0] as usize;
601            let matches = slot_name_len == name.len()
602                && &slot[1..1 + slot_name_len.min(MAX_CHANNEL_NAME_LEN)] == name;
603            if !matches {
604                new_buf[new_n..new_n + CH_NAME_SLOT_LEN].copy_from_slice(slot);
605                new_n += CH_NAME_SLOT_LEN;
606            }
607        }
608        if new_n == 0 {
609            let _ = self.delete_bytes(CH_INDEX_KEY).await;
610        } else {
611            self.store_bytes(CH_INDEX_KEY, &new_buf[..new_n]).await?;
612        }
613        Ok(())
614    }
615}
616
617/// View implementing [`umsh_hal::ChannelStore`] on top of a shared
618/// [`FlashStore`].
619pub struct ChannelView<F, M>
620where
621    F: MultiwriteNorFlash + 'static,
622    M: RawMutex + 'static,
623{
624    storage: &'static FlashStore<F, M>,
625}
626
627impl<F, M> ChannelView<F, M>
628where
629    F: MultiwriteNorFlash + 'static,
630    M: RawMutex + 'static,
631{
632    /// Construct a channel-store view over the shared static storage.
633    pub fn new(storage: &'static FlashStore<F, M>) -> Self {
634        Self { storage }
635    }
636}
637
638impl<F, M> umsh_hal::ChannelStore for ChannelView<F, M>
639where
640    F: MultiwriteNorFlash + 'static,
641    M: RawMutex + 'static,
642{
643    type Error = Error<F::Error>;
644
645    async fn store_channel(&self, name: &[u8], key: &[u8; 32]) -> Result<(), Self::Error> {
646        self.storage.store_channel_entry(name, key).await
647    }
648
649    async fn delete_channel(&self, name: &[u8]) -> Result<(), Self::Error> {
650        self.storage.delete_channel_entry(name).await
651    }
652
653    async fn for_each_channel(
654        &self,
655        f: &mut dyn FnMut(&[u8], &[u8; 32]),
656    ) -> Result<(), Self::Error> {
657        let mut buf: heapless::Vec<
658            (heapless::String<MAX_CHANNEL_NAME_LEN>, [u8; 32]),
659            MAX_CHANNELS,
660        > = heapless::Vec::new();
661        self.storage.load_all_channels(&mut buf).await?;
662        for (name, key) in buf.iter() {
663            f(name.as_bytes(), key);
664        }
665        Ok(())
666    }
667}
668
669/// View implementing [`umsh_hal::PeerStore`] on top of a shared
670/// [`FlashStore`]. Follows the same view-type pattern as
671/// [`KeyValueView`] and [`CounterView`].
672pub struct PeerView<F, M>
673where
674    F: MultiwriteNorFlash + 'static,
675    M: RawMutex + 'static,
676{
677    storage: &'static FlashStore<F, M>,
678}
679
680impl<F, M> PeerView<F, M>
681where
682    F: MultiwriteNorFlash + 'static,
683    M: RawMutex + 'static,
684{
685    /// Construct a peer-store view over the shared static storage.
686    pub fn new(storage: &'static FlashStore<F, M>) -> Self {
687        Self { storage }
688    }
689}
690
691impl<F, M> umsh_hal::PeerStore for PeerView<F, M>
692where
693    F: MultiwriteNorFlash + 'static,
694    M: RawMutex + 'static,
695{
696    type Error = Error<F::Error>;
697
698    async fn store_peer(&self, key: &[u8; 32], alias: Option<&[u8]>) -> Result<(), Self::Error> {
699        self.storage.store_peer_entry(key, alias).await
700    }
701
702    async fn delete_peer(&self, key: &[u8; 32]) -> Result<(), Self::Error> {
703        self.storage.delete_peer_entry(key).await
704    }
705
706    async fn for_each_peer(
707        &self,
708        f: &mut dyn FnMut(&[u8; 32], Option<&[u8]>),
709    ) -> Result<(), Self::Error> {
710        let mut buf: heapless::Vec<([u8; 32], Option<heapless::String<MAX_ALIAS_LEN>>), MAX_PEERS> =
711            heapless::Vec::new();
712        self.storage.load_all_peers(&mut buf).await?;
713        for (pk, alias) in buf.iter() {
714            f(pk, alias.as_ref().map(|s| s.as_bytes()));
715        }
716        Ok(())
717    }
718}
719
720/// View implementing [`umsh_hal::KeyValueStore`] on top of a shared
721/// [`FlashStore`]. The view itself is essentially a thin pointer; the
722/// real storage lives in the static [`FlashStore`].
723pub struct KeyValueView<F, M>
724where
725    F: MultiwriteNorFlash + 'static,
726    M: RawMutex + 'static,
727{
728    storage: &'static FlashStore<F, M>,
729}
730
731impl<F, M> KeyValueView<F, M>
732where
733    F: MultiwriteNorFlash + 'static,
734    M: RawMutex + 'static,
735{
736    /// Construct a KV view over the shared static storage.
737    pub fn new(storage: &'static FlashStore<F, M>) -> Self {
738        Self { storage }
739    }
740}
741
742impl<F, M> umsh_hal::KeyValueStore for KeyValueView<F, M>
743where
744    F: MultiwriteNorFlash + 'static,
745    M: RawMutex + 'static,
746{
747    type Error = Error<F::Error>;
748
749    async fn load(&self, key: &[u8], buf: &mut [u8]) -> Result<Option<usize>, Self::Error> {
750        self.storage.load_bytes(key, buf).await
751    }
752
753    async fn store(&self, key: &[u8], value: &[u8]) -> Result<(), Self::Error> {
754        self.storage.store_bytes(key, value).await
755    }
756
757    async fn delete(&self, key: &[u8]) -> Result<(), Self::Error> {
758        self.storage.delete_bytes(key).await
759    }
760}
761
762/// View implementing [`umsh_hal::CounterStore`] on top of a shared
763/// [`FlashStore`]. Counters are stored as little-endian u32 values
764/// keyed by the caller-supplied context bytes (the MAC layer is
765/// expected to prefix them with `mac.tx:` / `mac.rx:`).
766pub struct CounterView<F, M>
767where
768    F: MultiwriteNorFlash + 'static,
769    M: RawMutex + 'static,
770{
771    storage: &'static FlashStore<F, M>,
772}
773
774impl<F, M> CounterView<F, M>
775where
776    F: MultiwriteNorFlash + 'static,
777    M: RawMutex + 'static,
778{
779    /// Construct a counter view over the shared static storage.
780    pub fn new(storage: &'static FlashStore<F, M>) -> Self {
781        Self { storage }
782    }
783}
784
785impl<F, M> umsh_hal::CounterStore for CounterView<F, M>
786where
787    F: MultiwriteNorFlash + 'static,
788    M: RawMutex + 'static,
789{
790    type Error = Error<F::Error>;
791
792    async fn load(&self, context: &[u8]) -> Result<u32, Self::Error> {
793        let mut buf = [0u8; 4];
794        match self.storage.load_bytes(context, &mut buf).await? {
795            Some(4) => Ok(u32::from_le_bytes(buf)),
796            // Missing entry, or a corrupt one of unexpected size — treat
797            // as "no boundary persisted yet" so the MAC layer reseeds.
798            Some(_) | None => Ok(0),
799        }
800    }
801
802    async fn store(&self, context: &[u8], value: u32) -> Result<(), Self::Error> {
803        let bytes = value.to_le_bytes();
804        self.storage.store_bytes(context, &bytes).await
805    }
806
807    async fn flush(&self) -> Result<(), Self::Error> {
808        // `sequential-storage` commits synchronously inside `store_item`,
809        // so there is no deferred queue to drain here. The `flush`
810        // method exists in `umsh_hal::CounterStore` for backends that
811        // batch in RAM; for us it is a no-op.
812        Ok(())
813    }
814}