Skip to main content

nros_params/
server.rs

1//! Parameter server implementation
2//!
3//! Provides a static storage parameter server for embedded systems.
4//! Parameters are stored in a fixed-size array with compile-time capacity.
5
6use crate::types::{
7    MAX_PARAM_NAME_LEN, Parameter, ParameterDescriptor, ParameterType, ParameterValue,
8    SetParameterResult,
9};
10use heapless::String;
11
12// MAX_PARAMETERS is generated by build.rs and included via types.rs.
13use crate::types::MAX_PARAMETERS;
14
15/// Entry in the parameter storage.
16///
17/// phase-382 W2' — crate-private ON PURPOSE. The storage is caller-owned now,
18/// but the SLOT layout is not part of the deal: consumers name
19/// [`ParameterTable`] / [`ParameterStorage`], never this.
20#[derive(Debug, Clone)]
21pub(crate) struct ParameterEntry {
22    /// The parameter (name + value)
23    param: Parameter,
24    /// Optional descriptor with constraints
25    descriptor: Option<ParameterDescriptor>,
26}
27
28/// The slot table a [`ParameterServer`] borrows.
29///
30/// phase-382 W2' — the server's storage is CALLER-OWNED, and this newtype is
31/// what makes that expressible without publishing the slot type. A slot is a
32/// `ParameterEntry`, which stays PRIVATE (it is the store's internal slot
33/// layout, not an API); a consumer that only needs to *name* the storage —
34/// `nros-node`, which carves it out of the executor backing in W3' — names a
35/// `ParameterTable` instead.
36///
37/// **The table's LENGTH is the server's capacity.** [`MAX_PARAMETERS`] is the
38/// DEFAULT capacity (see [`ParameterStorage`]), not a bound the borrow
39/// re-imposes: a caller who hands over a longer or shorter table gets exactly
40/// that many slots, and every capacity question the server answers
41/// ([`is_full`](ParameterServer::is_full), the declare path) reads it from
42/// here.
43pub struct ParameterTable<'s> {
44    entries: &'s mut [Option<ParameterEntry>],
45}
46
47impl<'s> ParameterTable<'s> {
48    /// Wrap a caller-owned slice of slots.
49    pub(crate) fn new(entries: &'s mut [Option<ParameterEntry>]) -> Self {
50        Self { entries }
51    }
52
53    /// Number of slots — the capacity of a server built on this table.
54    pub fn capacity(&self) -> usize {
55        self.entries.len()
56    }
57
58    /// Number of slots currently holding a parameter.
59    pub fn occupied(&self) -> usize {
60        self.entries.iter().filter(|slot| slot.is_some()).count()
61    }
62}
63
64impl core::fmt::Debug for ParameterTable<'_> {
65    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
66        f.debug_struct("ParameterTable")
67            .field("capacity", &self.capacity())
68            .finish_non_exhaustive()
69    }
70}
71
72/// Backing storage a caller places, then lends to a [`ParameterServer`].
73///
74/// `N` defaults to [`MAX_PARAMETERS`], the build-time knob
75/// (`NROS_MAX_PARAMETERS` / `CONFIG_NROS_MAX_PARAMETERS`). It is a DEFAULT,
76/// not a cap — `ParameterStorage::<8>::new()` and `ParameterStorage::<256>`
77/// are equally valid, and the server takes its capacity from whatever table it
78/// is handed.
79///
80/// The intended shapes are a `static`, a caller-owned struct field, or (with
81/// `alloc`) one allocation made once at start-up:
82///
83/// ```
84/// use nros_params::{ParameterServer, ParameterStorage, ParameterValue};
85///
86/// let mut storage: ParameterStorage<8> = ParameterStorage::new();
87/// let mut server = ParameterServer::new_in(storage.as_table());
88/// assert!(server.declare("max_speed", ParameterValue::Double(1.0)));
89/// ```
90///
91/// # Why this type exists at all — issue 0756
92///
93/// This is where the size lives, and it is enormous: `ParameterValue` is sized
94/// by its largest variant
95/// (`StringArray(Vec<String<MAX_STRING_VALUE_LEN>, MAX_ARRAY_LEN>)`), so every
96/// slot costs ~8.5 KiB whatever it actually holds. Measured: **285,184 bytes**
97/// at the default 32 slots, **2,281,472** at 256. (The eight extra bytes in
98/// issue 0756's 285,192 were the `count` that stayed behind on
99/// [`ParameterServer`], which is 24 bytes total now.)
100///
101/// Before phase-382 W2' that bulk was INSIDE `ParameterServer`, so
102/// `Box::new(ParameterServer::new())` built a multi-hundred-KiB value on the
103/// STACK before copying it into the allocation — Rust has no placement-new —
104/// which silently overran a thread stack sized for anything smaller. On the
105/// Zephyr lane an image built with 256 slots booted to
106/// `dds_create_participant` and hung with no fault and no output, while 32 —
107/// which fits the 512 KiB main stack the cyclonedds snippet asks for — ran
108/// clean. The mitigation then was `ParameterServer::init_in_place`, a
109/// placement initialiser that bounded the largest temporary at one
110/// `Option<ParameterEntry>`.
111///
112/// W2' dissolves the *server* half of that: `ParameterServer` is now a table
113/// borrow plus a count (tens of bytes), so nothing about constructing a server
114/// depends on the knob. The bulk did not vanish, it MOVED here — and so did
115/// the hazard, verbatim: `Box::new(ParameterStorage::new())` is the same
116/// stack-first copy the issue was about. [`init_in_place`] is therefore kept,
117/// on the type that is actually big, and any heap-placed storage must go
118/// through it. A caller who places the storage as a `static` or lets W3' carve
119/// it out of the executor backing never materialises it by value and does not
120/// need it.
121///
122/// [`init_in_place`]: ParameterStorage::init_in_place
123pub struct ParameterStorage<const N: usize = MAX_PARAMETERS> {
124    entries: [Option<ParameterEntry>; N],
125}
126
127impl<const N: usize> ParameterStorage<N> {
128    /// Create empty storage.
129    ///
130    /// `const`, so a `static NROS_PARAMS: ParameterStorage = ParameterStorage::new();`
131    /// lands in `.bss` with no initialiser and no stack temporary.
132    // No `Default`: it would spell this same construction as a by-value
133    // expression, which is exactly the `Box::new(Default::default())` shape
134    // issue 0756 is about (see the type docs). Callers name `new()` — or
135    // `init_in_place` when the home is a heap allocation.
136    #[allow(clippy::new_without_default)]
137    #[allow(clippy::large_stack_arrays)] // Intentional: this IS the storage.
138    pub const fn new() -> Self {
139        Self {
140            entries: [const { None }; N],
141        }
142    }
143
144    /// Slots in this storage.
145    pub const fn capacity(&self) -> usize {
146        N
147    }
148
149    /// Lend the storage to a [`ParameterServer`].
150    pub fn as_table(&mut self) -> ParameterTable<'_> {
151        ParameterTable::new(&mut self.entries)
152    }
153
154    /// Initialise storage directly into `dst`, without ever materialising one
155    /// by value.
156    ///
157    /// Issue 0756 — see this type's documentation for why that matters. The
158    /// short version: this type is 285,184 bytes at the default 32 slots and
159    /// 2,281,472 at 256, and Rust has no placement-new, so
160    /// `Box::new(ParameterStorage::new())` builds all of it on the caller's
161    /// stack first. Initialising through the allocation bounds the largest
162    /// stack temporary at one `Option<ParameterEntry>`, so the
163    /// `NROS_MAX_PARAMETERS` knob no longer decides whether boot survives.
164    ///
165    /// # Safety
166    ///
167    /// `dst` must be non-null, correctly aligned, and valid for writes of
168    /// `size_of::<Self>()` bytes. The pointee may be uninitialised on entry;
169    /// it is fully initialised on return.
170    pub unsafe fn init_in_place(dst: *mut Self) {
171        // addr_of_mut! throughout: `(*dst).entries` as a place expression
172        // would require the pointee to already be initialised.
173        // Safety: delegated to this function's own contract on `dst`.
174        unsafe {
175            let entries = core::ptr::addr_of_mut!((*dst).entries).cast::<Option<ParameterEntry>>();
176            for i in 0..N {
177                entries.add(i).write(None);
178            }
179        }
180    }
181}
182
183/// Parameter server over caller-owned storage
184///
185/// Stores parameters in a [`ParameterTable`] the caller lends it; the table's
186/// length is the capacity. [`ParameterStorage`] is the usual way to produce
187/// one, defaulting to the build-time `MAX_PARAMETERS` slots.
188///
189/// # Example
190///
191/// ```
192/// use nros_params::{ParameterServer, ParameterStorage, ParameterValue};
193///
194/// let mut storage = ParameterStorage::<8>::new();
195/// let mut server = ParameterServer::new_in(storage.as_table());
196///
197/// // Declare a parameter with initial value
198/// server.declare("max_speed", ParameterValue::Double(1.0));
199///
200/// // Get parameter value
201/// if let Some(value) = server.get("max_speed") {
202///     println!("max_speed = {:?}", value.as_double());
203/// }
204///
205/// // Set parameter value
206/// server.set("max_speed", ParameterValue::Double(2.0));
207/// ```
208pub struct ParameterServer<'s> {
209    /// Borrowed parameter storage
210    table: ParameterTable<'s>,
211    /// Number of parameters currently stored
212    count: usize,
213}
214
215impl<'s> ParameterServer<'s> {
216    /// Create a parameter server over `table`.
217    ///
218    /// The server ADOPTS whatever the table already holds: `count` is
219    /// recovered from the occupied slots rather than assumed zero. That is
220    /// what makes a table safe to hand to a second server after the first is
221    /// dropped (and what lets storage arrive pre-seeded), where assuming empty
222    /// would desynchronise the count from the slots — `is_full` wrong in one
223    /// direction, `remove` underflowing in the other. Fresh
224    /// [`ParameterStorage`] is all-`None`, so the usual path costs one scan
225    /// and starts at zero.
226    pub fn new_in(table: ParameterTable<'s>) -> Self {
227        let count = table.occupied();
228        Self { table, count }
229    }
230
231    /// Get the number of parameters stored
232    pub fn len(&self) -> usize {
233        self.count
234    }
235
236    /// Check if the server has no parameters
237    pub fn is_empty(&self) -> bool {
238        self.count == 0
239    }
240
241    /// Capacity of the borrowed table.
242    ///
243    /// phase-382 W2' — this is the TABLE's length, not `MAX_PARAMETERS`. The
244    /// build-time knob only picks the default size of a [`ParameterStorage`].
245    pub fn capacity(&self) -> usize {
246        self.table.capacity()
247    }
248
249    /// Check if the server is at capacity
250    pub fn is_full(&self) -> bool {
251        self.count >= self.table.capacity()
252    }
253
254    /// Find the index of a parameter by name
255    fn find_index(&self, name: &str) -> Option<usize> {
256        self.table.entries.iter().position(|entry| {
257            entry
258                .as_ref()
259                .map(|e| e.param.name.as_str() == name)
260                .unwrap_or(false)
261        })
262    }
263
264    /// Find an empty slot
265    fn find_empty_slot(&self) -> Option<usize> {
266        self.table.entries.iter().position(|entry| entry.is_none())
267    }
268
269    /// Declare a new parameter with a value
270    ///
271    /// If the parameter already exists, this does nothing and returns false.
272    /// Returns true if the parameter was declared successfully.
273    pub fn declare(&mut self, name: &str, value: ParameterValue) -> bool {
274        self.declare_with_descriptor(name, value, None)
275    }
276
277    /// Declare a new parameter with value and descriptor
278    ///
279    /// The descriptor provides metadata like description, constraints, and read-only flag.
280    pub fn declare_with_descriptor(
281        &mut self,
282        name: &str,
283        value: ParameterValue,
284        descriptor: Option<ParameterDescriptor>,
285    ) -> bool {
286        // Check if already exists
287        if self.find_index(name).is_some() {
288            return false;
289        }
290
291        // Find an empty slot
292        let slot = match self.find_empty_slot() {
293            Some(idx) => idx,
294            None => return false,
295        };
296
297        // Create the parameter
298        let param = match Parameter::new(name, value) {
299            Some(p) => p,
300            None => return false,
301        };
302
303        self.table.entries[slot] = Some(ParameterEntry { param, descriptor });
304        self.count += 1;
305        true
306    }
307
308    /// Get a parameter value by name
309    pub fn get(&self, name: &str) -> Option<&ParameterValue> {
310        self.find_index(name)
311            .and_then(|idx| self.table.entries[idx].as_ref())
312            .map(|entry| &entry.param.value)
313    }
314
315    /// Get a parameter by name
316    pub fn get_parameter(&self, name: &str) -> Option<&Parameter> {
317        self.find_index(name)
318            .and_then(|idx| self.table.entries[idx].as_ref())
319            .map(|entry| &entry.param)
320    }
321
322    /// Get a parameter descriptor by name
323    pub fn get_descriptor(&self, name: &str) -> Option<&ParameterDescriptor> {
324        self.find_index(name)
325            .and_then(|idx| self.table.entries[idx].as_ref())
326            .and_then(|entry| entry.descriptor.as_ref())
327    }
328
329    /// Set a parameter value
330    ///
331    /// Returns the result of the set operation.
332    pub fn set(&mut self, name: &str, value: ParameterValue) -> SetParameterResult {
333        let idx = match self.find_index(name) {
334            Some(idx) => idx,
335            None => return SetParameterResult::NotFound,
336        };
337
338        let entry = match self.table.entries[idx].as_mut() {
339            Some(e) => e,
340            None => return SetParameterResult::NotFound,
341        };
342
343        // Check if read-only
344        if let Some(ref desc) = entry.descriptor {
345            if desc.read_only {
346                return SetParameterResult::ReadOnly;
347            }
348
349            // Check type compatibility
350            if !desc.dynamic_typing && desc.param_type != value.param_type() {
351                return SetParameterResult::TypeMismatch;
352            }
353
354            // Check range constraints
355            if !desc.validate_range(&value) {
356                return SetParameterResult::OutOfRange;
357            }
358        }
359
360        entry.param.value = value;
361        SetParameterResult::Success
362    }
363
364    /// Unset an optional parameter (set to NotSet)
365    ///
366    /// This bypasses the type check to allow optional parameters to be unset.
367    /// Returns an error if the parameter is read-only.
368    pub fn unset(&mut self, name: &str) -> SetParameterResult {
369        let idx = match self.find_index(name) {
370            Some(idx) => idx,
371            None => return SetParameterResult::NotFound,
372        };
373
374        let entry = match self.table.entries[idx].as_mut() {
375            Some(e) => e,
376            None => return SetParameterResult::NotFound,
377        };
378
379        // Check if read-only
380        if let Some(ref desc) = entry.descriptor
381            && desc.read_only
382        {
383            return SetParameterResult::ReadOnly;
384        }
385
386        entry.param.value = ParameterValue::NotSet;
387        SetParameterResult::Success
388    }
389
390    /// Set or declare a parameter
391    ///
392    /// If the parameter exists, sets its value. Otherwise, declares it.
393    pub fn set_or_declare(&mut self, name: &str, value: ParameterValue) -> SetParameterResult {
394        if self.find_index(name).is_some() {
395            self.set(name, value)
396        } else if self.declare(name, value) {
397            SetParameterResult::Success
398        } else {
399            SetParameterResult::StorageFull
400        }
401    }
402
403    /// Check if a parameter exists
404    pub fn has(&self, name: &str) -> bool {
405        self.find_index(name).is_some()
406    }
407
408    /// Remove a parameter
409    ///
410    /// Returns true if the parameter was removed.
411    pub fn remove(&mut self, name: &str) -> bool {
412        if let Some(idx) = self.find_index(name) {
413            self.table.entries[idx] = None;
414            self.count -= 1;
415            true
416        } else {
417            false
418        }
419    }
420
421    /// Get the type of a parameter
422    pub fn get_type(&self, name: &str) -> Option<ParameterType> {
423        self.get(name).map(|v| v.param_type())
424    }
425
426    /// Iterate over all parameters
427    pub fn iter(&self) -> impl Iterator<Item = &Parameter> {
428        self.table
429            .entries
430            .iter()
431            .filter_map(|entry| entry.as_ref().map(|e| &e.param))
432    }
433
434    /// List all parameter names
435    pub fn list_names(&self) -> impl Iterator<Item = &str> {
436        self.iter().map(|p| p.name.as_str())
437    }
438
439    /// List parameter names with a given prefix
440    pub fn list_with_prefix<'a>(&'a self, prefix: &'a str) -> impl Iterator<Item = &'a str> {
441        self.list_names()
442            .filter(move |name| name.starts_with(prefix))
443    }
444
445    /// Get a bool parameter value
446    pub fn get_bool(&self, name: &str) -> Option<bool> {
447        self.get(name).and_then(|v| v.as_bool())
448    }
449
450    /// Get an integer parameter value
451    pub fn get_integer(&self, name: &str) -> Option<i64> {
452        self.get(name).and_then(|v| v.as_integer())
453    }
454
455    /// Get a double parameter value
456    pub fn get_double(&self, name: &str) -> Option<f64> {
457        self.get(name).and_then(|v| v.as_double())
458    }
459
460    /// Get a string parameter value
461    pub fn get_string(&self, name: &str) -> Option<&str> {
462        self.get(name).and_then(|v| v.as_string())
463    }
464
465    /// Set a bool parameter value
466    pub fn set_bool(&mut self, name: &str, value: bool) -> SetParameterResult {
467        self.set(name, ParameterValue::Bool(value))
468    }
469
470    /// Set an integer parameter value
471    pub fn set_integer(&mut self, name: &str, value: i64) -> SetParameterResult {
472        self.set(name, ParameterValue::Integer(value))
473    }
474
475    /// Set a double parameter value
476    pub fn set_double(&mut self, name: &str, value: f64) -> SetParameterResult {
477        self.set(name, ParameterValue::Double(value))
478    }
479
480    /// Set a string parameter value
481    pub fn set_string(&mut self, name: &str, value: &str) -> SetParameterResult {
482        match ParameterValue::from_string(value) {
483            Some(v) => self.set(name, v),
484            None => SetParameterResult::StorageFull, // String too long
485        }
486    }
487
488    /// Declare a parameter with a descriptor and optional initial value.
489    ///
490    /// This is a more comprehensive declaration method used by the typed parameter API.
491    ///
492    /// # Arguments
493    /// - `descriptor`: The metadata for the parameter.
494    /// - `initial_value`: An optional initial value for the parameter. If `None`,
495    ///   the parameter will be initialized to `ParameterValue::NotSet`.
496    pub fn declare_parameter(
497        &mut self,
498        descriptor: ParameterDescriptor,
499        initial_value: Option<&ParameterValue>,
500    ) -> Result<(), SetParameterResult> {
501        let name = descriptor.name.as_str();
502
503        // Check if already exists
504        if self.find_index(name).is_some() {
505            return Err(SetParameterResult::TypeMismatch); // Indicates already declared
506        }
507
508        // Find an empty slot
509        let slot = match self.find_empty_slot() {
510            Some(idx) => idx,
511            None => return Err(SetParameterResult::StorageFull),
512        };
513
514        let param_value = initial_value.cloned().unwrap_or_default();
515
516        let param = Parameter::new(name, param_value).ok_or(SetParameterResult::StorageFull)?; // name too long
517
518        self.table.entries[slot] = Some(ParameterEntry {
519            param,
520            descriptor: Some(descriptor),
521        });
522        self.count += 1;
523        Ok(())
524    }
525
526    /// Get the current value of a parameter.
527    ///
528    /// Returns `Some(ParameterValue)` if the parameter exists, `None` otherwise.
529    /// The `ParameterValue` is cloned to avoid lifetime issues with `&mut self`.
530    pub fn get_parameter_value(&self, name: &str) -> Option<ParameterValue> {
531        self.get(name).cloned()
532    }
533
534    /// Set the value of a parameter.
535    ///
536    /// Returns `SetParameterResult::Success` on success, or an error if the parameter
537    /// is read-only, type mismatches, or is out of range.
538    pub fn set_parameter_value(&mut self, name: &str, value: ParameterValue) -> SetParameterResult {
539        self.set(name, value)
540    }
541}
542
543/// Builder for declaring parameters with a fluent API
544pub struct LegacyParameterBuilder<'a, 's> {
545    server: &'a mut ParameterServer<'s>,
546    name: String<MAX_PARAM_NAME_LEN>,
547    value: ParameterValue,
548    descriptor: Option<ParameterDescriptor>,
549}
550
551impl<'a, 's> LegacyParameterBuilder<'a, 's> {
552    /// Create a new parameter builder
553    pub fn new(
554        server: &'a mut ParameterServer<'s>,
555        name: &str,
556        value: ParameterValue,
557    ) -> Option<Self> {
558        let mut n = String::new();
559        n.push_str(name).ok()?;
560        Some(Self {
561            server,
562            name: n,
563            value,
564            descriptor: None,
565        })
566    }
567
568    /// Set the parameter description
569    pub fn description(mut self, desc: &str) -> Self {
570        let param_type = self.value.param_type();
571        if self.descriptor.is_none() {
572            self.descriptor = ParameterDescriptor::new(self.name.as_str(), param_type);
573        }
574        if let Some(ref mut d) = self.descriptor {
575            d.description.clear();
576            let _ = d.description.push_str(desc);
577        }
578        self
579    }
580
581    /// Set the parameter as read-only
582    pub fn read_only(mut self) -> Self {
583        let param_type = self.value.param_type();
584        if self.descriptor.is_none() {
585            self.descriptor = ParameterDescriptor::new(self.name.as_str(), param_type);
586        }
587        if let Some(ref mut d) = self.descriptor {
588            d.read_only = true;
589        }
590        self
591    }
592
593    /// Set integer range constraints
594    pub fn integer_range(mut self, min: i64, max: i64, step: i64) -> Self {
595        let param_type = self.value.param_type();
596        if self.descriptor.is_none() {
597            self.descriptor = ParameterDescriptor::new(self.name.as_str(), param_type);
598        }
599        if let Some(ref mut d) = self.descriptor {
600            d.range = crate::types::ParameterRange::Integer(crate::types::IntegerRange::new(
601                min, max, step,
602            ));
603        }
604        self
605    }
606
607    /// Set float range constraints
608    pub fn float_range(mut self, min: f64, max: f64, step: f64) -> Self {
609        let param_type = self.value.param_type();
610        if self.descriptor.is_none() {
611            self.descriptor = ParameterDescriptor::new(self.name.as_str(), param_type);
612        }
613        if let Some(ref mut d) = self.descriptor {
614            d.range = crate::types::ParameterRange::FloatingPoint(
615                crate::types::FloatingPointRange::new(min, max, step),
616            );
617        }
618        self
619    }
620
621    /// Declare the parameter
622    pub fn declare(self) -> bool {
623        self.server
624            .declare_with_descriptor(self.name.as_str(), self.value, self.descriptor)
625    }
626}
627
628#[cfg(test)]
629mod tests {
630    use super::*;
631
632    #[test]
633    fn test_new_server() {
634        let mut storage: ParameterStorage = ParameterStorage::new();
635        let server = ParameterServer::new_in(storage.as_table());
636        assert_eq!(server.len(), 0);
637        assert!(server.is_empty());
638    }
639
640    #[test]
641    #[allow(clippy::approx_constant)]
642    fn test_declare_and_get() {
643        let mut storage: ParameterStorage = ParameterStorage::new();
644        let mut server = ParameterServer::new_in(storage.as_table());
645
646        assert!(server.declare("my_bool", ParameterValue::Bool(true)));
647        assert!(server.declare("my_int", ParameterValue::Integer(42)));
648        assert!(server.declare("my_double", ParameterValue::Double(3.14)));
649
650        assert_eq!(server.len(), 3);
651        assert!(!server.is_empty());
652
653        assert_eq!(server.get_bool("my_bool"), Some(true));
654        assert_eq!(server.get_integer("my_int"), Some(42));
655        assert_eq!(server.get_double("my_double"), Some(3.14));
656    }
657
658    #[test]
659    fn test_unset_clears_the_value_and_reports_success() {
660        // phase-359 W10 / issue 0080 — what survives of
661        // `test_dirty_tracks_value_changes_not_declarations`. That test existed
662        // for the persistence flush's dirty flag, which is deleted with the
663        // seam; but it carried the crate's ONLY coverage of `unset`, so the
664        // assertion moves here rather than going out with the flag.
665        let mut storage: ParameterStorage = ParameterStorage::new();
666        let mut server = ParameterServer::new_in(storage.as_table());
667        assert!(server.declare("count", ParameterValue::Integer(0)));
668        assert_eq!(server.set_integer("count", 10), SetParameterResult::Success);
669        assert_eq!(server.get_integer("count"), Some(10));
670
671        assert_eq!(server.unset("count"), SetParameterResult::Success);
672        assert_eq!(server.get_integer("count"), None);
673    }
674
675    #[test]
676    fn test_set_parameter() {
677        let mut storage: ParameterStorage = ParameterStorage::new();
678        let mut server = ParameterServer::new_in(storage.as_table());
679        server.declare("count", ParameterValue::Integer(0));
680
681        assert_eq!(server.set_integer("count", 10), SetParameterResult::Success);
682        assert_eq!(server.get_integer("count"), Some(10));
683    }
684
685    #[test]
686    fn test_set_nonexistent() {
687        let mut storage: ParameterStorage = ParameterStorage::new();
688        let mut server = ParameterServer::new_in(storage.as_table());
689        assert_eq!(
690            server.set("nonexistent", ParameterValue::Integer(1)),
691            SetParameterResult::NotFound
692        );
693    }
694
695    #[test]
696    fn test_read_only_parameter() {
697        let mut storage: ParameterStorage = ParameterStorage::new();
698        let mut server = ParameterServer::new_in(storage.as_table());
699
700        let desc = ParameterDescriptor::new("version", ParameterType::String)
701            .unwrap()
702            .with_read_only(true);
703
704        server.declare_with_descriptor(
705            "version",
706            ParameterValue::from_string("1.0.0").unwrap(),
707            Some(desc),
708        );
709
710        assert_eq!(
711            server.set_string("version", "2.0.0"),
712            SetParameterResult::ReadOnly
713        );
714        assert_eq!(server.get_string("version"), Some("1.0.0"));
715    }
716
717    #[test]
718    fn test_range_constraints() {
719        let mut storage: ParameterStorage = ParameterStorage::new();
720        let mut server = ParameterServer::new_in(storage.as_table());
721
722        let desc = ParameterDescriptor::new("speed", ParameterType::Double)
723            .unwrap()
724            .with_float_range(0.0, 10.0, 0.0);
725
726        server.declare_with_descriptor("speed", ParameterValue::Double(5.0), Some(desc));
727
728        // Valid value
729        assert_eq!(server.set_double("speed", 8.0), SetParameterResult::Success);
730
731        // Out of range
732        assert_eq!(
733            server.set_double("speed", 15.0),
734            SetParameterResult::OutOfRange
735        );
736        assert_eq!(server.get_double("speed"), Some(8.0)); // Unchanged
737    }
738
739    #[test]
740    fn test_remove_parameter() {
741        let mut storage: ParameterStorage = ParameterStorage::new();
742        let mut server = ParameterServer::new_in(storage.as_table());
743        server.declare("temp", ParameterValue::Double(25.0));
744
745        assert!(server.has("temp"));
746        assert!(server.remove("temp"));
747        assert!(!server.has("temp"));
748        assert!(!server.remove("temp")); // Already removed
749    }
750
751    #[test]
752    fn test_list_parameters() {
753        let mut storage: ParameterStorage = ParameterStorage::new();
754        let mut server = ParameterServer::new_in(storage.as_table());
755        server.declare("robot.speed", ParameterValue::Double(1.0));
756        server.declare("robot.name", ParameterValue::from_string("bot1").unwrap());
757        server.declare("sensor.range", ParameterValue::Double(10.0));
758
759        let robot_params: heapless::Vec<&str, 8> = server.list_with_prefix("robot.").collect();
760        assert_eq!(robot_params.len(), 2);
761    }
762
763    #[test]
764    fn test_set_or_declare() {
765        let mut storage: ParameterStorage = ParameterStorage::new();
766        let mut server = ParameterServer::new_in(storage.as_table());
767
768        // First call declares
769        assert_eq!(
770            server.set_or_declare("new_param", ParameterValue::Integer(1)),
771            SetParameterResult::Success
772        );
773        assert_eq!(server.get_integer("new_param"), Some(1));
774
775        // Second call sets
776        assert_eq!(
777            server.set_or_declare("new_param", ParameterValue::Integer(2)),
778            SetParameterResult::Success
779        );
780        assert_eq!(server.get_integer("new_param"), Some(2));
781    }
782
783    #[test]
784    fn test_duplicate_declare() {
785        let mut storage: ParameterStorage = ParameterStorage::new();
786        let mut server = ParameterServer::new_in(storage.as_table());
787        assert!(server.declare("param", ParameterValue::Integer(1)));
788        assert!(!server.declare("param", ParameterValue::Integer(2))); // Already exists
789        assert_eq!(server.get_integer("param"), Some(1)); // Unchanged
790    }
791}
792
793// =============================================================================
794// Ghost model validation
795// =============================================================================
796
797#[cfg(test)]
798mod ghost_checks {
799    use super::*;
800    use nros_ghost_types::ParamServerGhost;
801
802    /// Structural check: construct ParamServerGhost from ParameterServer private fields.
803    /// If a field is renamed or retyped, this fails to compile.
804    fn ghost_from_server(s: &ParameterServer<'_>) -> ParamServerGhost {
805        ParamServerGhost {
806            count: s.count,
807            // phase-382 W2' — capacity is the borrowed TABLE's length. It used
808            // to read `MAX_PARAMETERS`, which was the same number only because
809            // the storage was the knob-sized array inside the server.
810            max: s.table.capacity(),
811        }
812    }
813
814    #[test]
815    fn ghost_new_count() {
816        let mut storage: ParameterStorage = ParameterStorage::new();
817        let capacity = storage.capacity();
818        let server = ParameterServer::new_in(storage.as_table());
819        let ghost = ghost_from_server(&server);
820        assert_eq!(ghost.count, 0);
821        // phase-382 W2' — the ghost's `max` is the TABLE's length, and the
822        // default-parameter `ParameterStorage` is `MAX_PARAMETERS` long. This
823        // used to be a literal `32`, which held only while the knob was at its
824        // default.
825        assert_eq!(ghost.max, capacity);
826        assert_eq!(ghost.max, MAX_PARAMETERS);
827    }
828
829    /// phase-382 W2' — capacity follows the TABLE, not `MAX_PARAMETERS`: a
830    /// server built on a short table is full at that table's length.
831    #[test]
832    fn ghost_max_follows_a_shorter_table() {
833        let mut storage = ParameterStorage::<2>::new();
834        let mut server = ParameterServer::new_in(storage.as_table());
835        assert_eq!(ghost_from_server(&server).max, 2);
836        assert!(server.declare("a", ParameterValue::Integer(1)));
837        assert!(server.declare("b", ParameterValue::Integer(2)));
838        assert!(server.is_full());
839        assert!(!server.declare("c", ParameterValue::Integer(3)));
840        let ghost = ghost_from_server(&server);
841        assert!(ghost.count <= ghost.max);
842    }
843
844    /// phase-382 W2' — `new_in` ADOPTS the table it is handed, so a second
845    /// server over the same storage sees the parameters the first declared
846    /// and keeps its count in step with the slots.
847    #[test]
848    fn ghost_count_recovers_from_a_reused_table() {
849        let mut storage = ParameterStorage::<4>::new();
850        {
851            let mut first = ParameterServer::new_in(storage.as_table());
852            assert!(first.declare("kept", ParameterValue::Integer(7)));
853        }
854        let second = ParameterServer::new_in(storage.as_table());
855        assert_eq!(ghost_from_server(&second).count, 1);
856        assert_eq!(second.get_integer("kept"), Some(7));
857    }
858
859    #[test]
860    fn ghost_declare_increments() {
861        let mut storage: ParameterStorage = ParameterStorage::new();
862        let mut server = ParameterServer::new_in(storage.as_table());
863        let before = ghost_from_server(&server).count;
864        assert!(server.declare("test", ParameterValue::Integer(1)));
865        let after = ghost_from_server(&server).count;
866        assert_eq!(after, before + 1);
867    }
868
869    #[test]
870    fn ghost_remove_decrements() {
871        let mut storage: ParameterStorage = ParameterStorage::new();
872        let mut server = ParameterServer::new_in(storage.as_table());
873        server.declare("test", ParameterValue::Integer(1));
874        let before = ghost_from_server(&server).count;
875        assert!(server.remove("test"));
876        let after = ghost_from_server(&server).count;
877        assert_eq!(after, before - 1);
878    }
879
880    #[test]
881    fn ghost_count_bounded() {
882        let mut storage: ParameterStorage = ParameterStorage::new();
883        let mut server = ParameterServer::new_in(storage.as_table());
884        for i in 0..MAX_PARAMETERS {
885            let mut name = heapless::String::<64>::new();
886            let _ = core::fmt::write(&mut name, format_args!("p{}", i));
887            server.declare(name.as_str(), ParameterValue::Integer(i as i64));
888        }
889        let ghost = ghost_from_server(&server);
890        assert!(ghost.count <= ghost.max);
891        assert_eq!(ghost.count, MAX_PARAMETERS);
892        // Next declare should fail — count stays bounded
893        assert!(!server.declare("overflow", ParameterValue::Integer(0)));
894        let ghost2 = ghost_from_server(&server);
895        assert!(ghost2.count <= ghost2.max);
896    }
897}
898
899// =============================================================================
900// Kani bounded model checking proofs
901// =============================================================================
902
903#[cfg(kani)]
904mod verification {
905    use super::*;
906
907    // phase-382 W2' — each proof used to build the whole store on the proof
908    // stack (`ParameterServer::new()`); it now places a `ParameterStorage` and
909    // lends it, which is the only shape a caller-owned store has. Kani still
910    // sees the same `MAX_PARAMETERS` slots, so the bounds are unchanged.
911
912    #[kani::proof]
913    fn server_new_is_empty() {
914        let mut storage: ParameterStorage = ParameterStorage::new();
915        let server = ParameterServer::new_in(storage.as_table());
916        assert!(server.is_empty());
917        assert_eq!(server.len(), 0);
918        assert!(!server.is_full());
919    }
920
921    #[kani::proof]
922    fn server_declare_get_roundtrip_integer() {
923        let mut storage: ParameterStorage = ParameterStorage::new();
924        let mut server = ParameterServer::new_in(storage.as_table());
925        let val: i64 = kani::any();
926        assert!(server.declare("test", ParameterValue::Integer(val)));
927        assert_eq!(server.get_integer("test"), Some(val));
928        assert!(server.has("test"));
929        assert_eq!(server.len(), 1);
930    }
931
932    #[kani::proof]
933    fn server_declare_get_roundtrip_bool() {
934        let mut storage: ParameterStorage = ParameterStorage::new();
935        let mut server = ParameterServer::new_in(storage.as_table());
936        let val: bool = kani::any();
937        assert!(server.declare("test", ParameterValue::Bool(val)));
938        assert_eq!(server.get_bool("test"), Some(val));
939    }
940
941    #[kani::proof]
942    fn server_set_requires_declare() {
943        let mut storage: ParameterStorage = ParameterStorage::new();
944        let mut server = ParameterServer::new_in(storage.as_table());
945        let val: i64 = kani::any();
946        // Set without declare should fail
947        let result = server.set("test", ParameterValue::Integer(val));
948        assert_eq!(result, SetParameterResult::NotFound);
949    }
950
951    #[kani::proof]
952    fn server_duplicate_declare_fails() {
953        let mut storage: ParameterStorage = ParameterStorage::new();
954        let mut server = ParameterServer::new_in(storage.as_table());
955        assert!(server.declare("test", ParameterValue::Integer(1)));
956        assert!(!server.declare("test", ParameterValue::Integer(2)));
957        // Original value preserved
958        assert_eq!(server.get_integer("test"), Some(1));
959    }
960
961    #[kani::proof]
962    fn server_remove_clears() {
963        let mut storage: ParameterStorage = ParameterStorage::new();
964        let mut server = ParameterServer::new_in(storage.as_table());
965        assert!(server.declare("test", ParameterValue::Integer(42)));
966        assert!(server.has("test"));
967        assert!(server.remove("test"));
968        assert!(!server.has("test"));
969        assert_eq!(server.len(), 0);
970    }
971
972    #[kani::proof]
973    fn server_get_nonexistent_returns_none() {
974        let mut storage: ParameterStorage = ParameterStorage::new();
975        let server = ParameterServer::new_in(storage.as_table());
976        assert!(server.get("nonexistent").is_none());
977        assert!(server.get_bool("nonexistent").is_none());
978        assert!(server.get_integer("nonexistent").is_none());
979        assert!(server.get_double("nonexistent").is_none());
980        assert!(server.get_string("nonexistent").is_none());
981    }
982}