Skip to main content

pamoja_sim/
sensor.rs

1//! Fake sensors that stand in for real hardware.
2
3use pamoja_core::{Error, Result, Sensor};
4
5/// A fake sensor that generates a signal from a baseline, drift, and noise.
6///
7/// This is the workhorse for hardware-free development: it implements the core
8/// [`Sensor`] trait, so it drops into a `Node`, a profile, or any test exactly where
9/// a real probe would, and it produces readings that look like the field rather than
10/// a clean constant. A reading is the baseline plus any accumulated drift plus a
11/// bounded pseudo-random wobble, so a control loop can be exercised against a signal
12/// that warms, sags, or jitters the way a real one does.
13///
14/// The noise is deterministic for a given seed - a small xorshift generator drives
15/// it, with no `rand` dependency - so a test that uses a `SimSensor` produces the
16/// same sequence every run and stays reproducible in CI.
17///
18/// # Examples
19///
20/// A noisy thermometer that warms by 0.1 degrees each reading:
21///
22/// ```
23/// use pamoja_core::Sensor;
24/// use pamoja_sim::SimSensor;
25///
26/// # async fn demo() -> pamoja_core::Result<()> {
27/// let mut probe = SimSensor::new(20.0).with_drift(0.1).with_noise(0.05).with_seed(7);
28/// let first = probe.read().await?;
29/// assert!((first - 20.0).abs() <= 0.05); // the first reading sits near the baseline
30/// # Ok(())
31/// # }
32/// ```
33#[derive(Clone, Copy, Debug)]
34pub struct SimSensor {
35    value: f32,
36    drift: f32,
37    noise: f32,
38    rng: u32,
39}
40
41impl SimSensor {
42    /// Creates a sensor that reads `baseline` with no drift or noise.
43    ///
44    /// # Arguments
45    ///
46    /// * `baseline` - the value the sensor reads before drift and noise are added.
47    ///
48    /// # Returns
49    ///
50    /// A steady sensor; add [`with_drift`](SimSensor::with_drift) and
51    /// [`with_noise`](SimSensor::with_noise) to make it lifelike.
52    pub fn new(baseline: f32) -> Self {
53        Self {
54            value: baseline,
55            drift: 0.0,
56            noise: 0.0,
57            rng: 0x9E37_79B9,
58        }
59    }
60
61    /// Sets how much the baseline moves each reading, modelling a slow trend.
62    ///
63    /// # Arguments
64    ///
65    /// * `per_read` - the amount added to the baseline after each reading; negative
66    ///   values sag the signal downward.
67    ///
68    /// # Returns
69    ///
70    /// The updated sensor, for chaining.
71    pub fn with_drift(mut self, per_read: f32) -> Self {
72        self.drift = per_read;
73        self
74    }
75
76    /// Sets the amplitude of the bounded noise added to each reading.
77    ///
78    /// # Arguments
79    ///
80    /// * `amplitude` - the largest magnitude the noise can reach; its magnitude is
81    ///   used, and each reading wobbles within plus or minus this amount.
82    ///
83    /// # Returns
84    ///
85    /// The updated sensor, for chaining.
86    pub fn with_noise(mut self, amplitude: f32) -> Self {
87        self.noise = magnitude(amplitude);
88        self
89    }
90
91    /// Sets the seed for the noise generator, making a run reproducible.
92    ///
93    /// # Arguments
94    ///
95    /// * `seed` - the generator seed; zero is replaced with a fixed non-zero value,
96    ///   since the xorshift generator cannot start from zero.
97    ///
98    /// # Returns
99    ///
100    /// The updated sensor, for chaining.
101    pub fn with_seed(mut self, seed: u32) -> Self {
102        self.rng = if seed == 0 { 1 } else { seed };
103        self
104    }
105
106    // Advances the xorshift generator and maps it to bounded noise.
107    fn next_noise(&mut self) -> f32 {
108        if self.noise == 0.0 {
109            return 0.0;
110        }
111        let mut x = self.rng;
112        x ^= x << 13;
113        x ^= x >> 17;
114        x ^= x << 5;
115        self.rng = x;
116        let unit = (x as f32 / u32::MAX as f32) * 2.0 - 1.0; // [-1.0, 1.0)
117        unit * self.noise
118    }
119}
120
121impl Sensor for SimSensor {
122    type Reading = f32;
123
124    async fn read(&mut self) -> Result<f32> {
125        let reading = self.value + self.next_noise();
126        self.value += self.drift;
127        Ok(reading)
128    }
129}
130
131/// A fake sensor that replays a fixed sequence of readings.
132///
133/// Where a [`SimSensor`] generates a signal, a `Replay` plays back exact values in
134/// order, which is what a deterministic test or a scripted demo wants: spell out the
135/// readings that tell the story, and the sensor yields them one per
136/// [`read`](Sensor::read). A one-shot replay reports [`Error::Closed`] once the
137/// sequence is exhausted; a repeating one loops forever.
138///
139/// # Examples
140///
141/// ```
142/// use pamoja_core::Sensor;
143/// use pamoja_sim::Replay;
144///
145/// # async fn demo() -> pamoja_core::Result<()> {
146/// let mut gauge = Replay::new(vec![1.0, 1.2, 1.9]);
147/// assert_eq!(gauge.read().await?, 1.0);
148/// assert_eq!(gauge.read().await?, 1.2);
149/// # Ok(())
150/// # }
151/// ```
152#[derive(Clone, Debug)]
153pub struct Replay {
154    readings: Vec<f32>,
155    index: usize,
156    repeat: bool,
157}
158
159impl Replay {
160    /// Creates a sensor that yields `readings` once, then reports closed.
161    ///
162    /// # Arguments
163    ///
164    /// * `readings` - the values to play back in order.
165    ///
166    /// # Returns
167    ///
168    /// A one-shot replay sensor.
169    pub fn new(readings: Vec<f32>) -> Self {
170        Self {
171            readings,
172            index: 0,
173            repeat: false,
174        }
175    }
176
177    /// Creates a sensor that yields `readings` in a loop forever.
178    ///
179    /// # Arguments
180    ///
181    /// * `readings` - the values to play back in order, repeating from the start.
182    ///
183    /// # Returns
184    ///
185    /// A repeating replay sensor.
186    pub fn repeating(readings: Vec<f32>) -> Self {
187        Self {
188            readings,
189            index: 0,
190            repeat: true,
191        }
192    }
193}
194
195impl Sensor for Replay {
196    type Reading = f32;
197
198    async fn read(&mut self) -> Result<f32> {
199        if self.index >= self.readings.len() {
200            if self.repeat && !self.readings.is_empty() {
201                self.index = 0;
202            } else {
203                return Err(Error::Closed);
204            }
205        }
206        let reading = self.readings[self.index];
207        self.index += 1;
208        Ok(reading)
209    }
210}
211
212// `f32::abs` lives in `std`, but a hand-rolled magnitude keeps this consistent with
213// the rest of the SDK's pure-logic crates.
214fn magnitude(value: f32) -> f32 {
215    if value < 0.0 {
216        -value
217    } else {
218        value
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[tokio::test]
227    async fn a_plain_sensor_returns_its_baseline() {
228        let mut sensor = SimSensor::new(3.5);
229        assert_eq!(sensor.read().await.unwrap(), 3.5);
230        assert_eq!(sensor.read().await.unwrap(), 3.5);
231    }
232
233    #[tokio::test]
234    async fn drift_accumulates_each_reading() {
235        let mut sensor = SimSensor::new(10.0).with_drift(2.0);
236        assert_eq!(sensor.read().await.unwrap(), 10.0);
237        assert_eq!(sensor.read().await.unwrap(), 12.0);
238        assert_eq!(sensor.read().await.unwrap(), 14.0);
239    }
240
241    #[tokio::test]
242    async fn the_same_seed_replays_the_same_noise() {
243        let mut a = SimSensor::new(20.0).with_noise(0.5).with_seed(7);
244        let mut b = SimSensor::new(20.0).with_noise(0.5).with_seed(7);
245        for _ in 0..16 {
246            assert_eq!(a.read().await.unwrap(), b.read().await.unwrap());
247        }
248    }
249
250    #[tokio::test]
251    async fn noise_stays_within_its_amplitude() {
252        let mut sensor = SimSensor::new(20.0).with_noise(0.5).with_seed(99);
253        for _ in 0..1000 {
254            let reading = sensor.read().await.unwrap();
255            assert!((reading - 20.0).abs() <= 0.5 + f32::EPSILON);
256        }
257    }
258
259    #[tokio::test]
260    async fn replay_yields_readings_in_order_then_closes() {
261        let mut sensor = Replay::new(vec![1.0, 2.0]);
262        assert_eq!(sensor.read().await.unwrap(), 1.0);
263        assert_eq!(sensor.read().await.unwrap(), 2.0);
264        assert!(matches!(sensor.read().await, Err(Error::Closed)));
265    }
266
267    #[tokio::test]
268    async fn a_repeating_replay_loops() {
269        let mut sensor = Replay::repeating(vec![1.0, 2.0]);
270        assert_eq!(sensor.read().await.unwrap(), 1.0);
271        assert_eq!(sensor.read().await.unwrap(), 2.0);
272        assert_eq!(sensor.read().await.unwrap(), 1.0);
273    }
274
275    #[tokio::test]
276    async fn an_empty_replay_is_closed() {
277        let mut sensor = Replay::repeating(vec![]);
278        assert!(matches!(sensor.read().await, Err(Error::Closed)));
279    }
280}