Skip to main content

pamoja_sensors/scd4x/
sim.rs

1//! An SCD40 or SCD41 that is not there.
2//!
3//! [`part`] answers the way an SCD4x reading 800 ppm at 22.5 C and 45 % does, and
4//! [`reporting`] at any reading. It answers at the one address the part has,
5//! [`I2C_ADDRESS`], and takes Sensirion's 16-bit commands: the serial number, the
6//! data-ready status, which always says a reading is waiting, and the reading itself, each
7//! word with its CRC; starting, stopping, and the other commands answer with nothing.
8
9use pamoja_hal::sim::CommandPart;
10
11use super::{command, command_frame, serial_number_frame, word_frame, Measurement, I2C_ADDRESS};
12
13/// The carbon dioxide concentration [`part`] reports, in parts per million.
14pub const CO2_PPM: u16 = 800;
15
16/// The temperature [`part`] reports.
17pub const CELSIUS: f32 = 22.5;
18
19/// The relative humidity [`part`] reports, as a percentage.
20pub const RELATIVE_HUMIDITY: f32 = 45.0;
21
22/// The serial number every simulated part reports.
23pub const SERIAL: u64 = 0x0000_5A4D_0C1E_2B3F;
24
25/// The data-ready word the part answers with: a reading is waiting.
26const DATA_READY: u16 = 0x0001;
27
28/// An SCD4x reading [`CO2_PPM`], [`CELSIUS`], and [`RELATIVE_HUMIDITY`].
29///
30/// # Returns
31///
32/// The part.
33#[must_use]
34pub fn part() -> CommandPart {
35    reporting(CO2_PPM, CELSIUS, RELATIVE_HUMIDITY)
36}
37
38/// An SCD4x that reads what it is asked to.
39///
40/// # Arguments
41///
42/// * `co2_ppm` - the carbon dioxide concentration it reports, in parts per million.
43/// * `celsius` - the temperature it reports.
44/// * `relative_humidity` - the humidity it reports, as a percentage.
45///
46/// # Returns
47///
48/// The part. Its temperature and humidity land on the nearest steps its 16-bit words
49/// represent.
50///
51/// # Examples
52///
53/// ```
54/// use pamoja_hal::script::DelayLog;
55/// use pamoja_sensors::scd4x::{sim, Scd4x};
56///
57/// let mut sensor = Scd4x::new(sim::reporting(1_450, 24.0, 55.0), DelayLog::new());
58/// let reading = sensor.measure().expect("the part answers");
59/// assert_eq!(reading.co2_ppm, 1_450);
60/// ```
61#[must_use]
62pub fn reporting(co2_ppm: u16, celsius: f32, relative_humidity: f32) -> CommandPart {
63    let milli_celsius = rounded(celsius * 1000.0);
64    let milli_percent = rounded(relative_humidity.max(0.0) * 1000.0) as u32;
65    let reading = Measurement::from_physical(co2_ppm, milli_celsius, milli_percent).to_bytes();
66    CommandPart::new(I2C_ADDRESS, 2)
67        .answering(
68            &command_frame(command::GET_SERIAL_NUMBER),
69            &serial_number_frame(SERIAL),
70        )
71        .answering(
72            &command_frame(command::GET_DATA_READY_STATUS),
73            &word_frame(DATA_READY),
74        )
75        .answering(&command_frame(command::READ_MEASUREMENT), &reading)
76}
77
78// Rounds away from zero, since a part cannot call `f32::round` without an operating system.
79fn rounded(value: f32) -> i32 {
80    if value >= 0.0 {
81        (value + 0.5) as i32
82    } else {
83        (value - 0.5) as i32
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use crate::scd4x::{data_ready, Scd4x};
91    use pamoja_hal::script::DelayLog;
92
93    #[test]
94    fn the_word_says_a_reading_is_waiting() {
95        assert!(data_ready(DATA_READY));
96    }
97
98    #[test]
99    fn the_driver_starts_the_part_and_reads_it() {
100        let mut sensor = Scd4x::new(part(), DelayLog::new());
101        let reading = sensor.measure().expect("a reading");
102        assert_eq!(reading.co2_ppm, CO2_PPM);
103        assert!((reading.celsius() - CELSIUS).abs() < 0.003);
104        assert!((reading.relative_humidity_percent() - RELATIVE_HUMIDITY).abs() < 0.002);
105        assert_eq!(sensor.serial(), Some(SERIAL));
106
107        let mut single = Scd4x::new(reporting(3_000, -5.0, 20.0), DelayLog::new());
108        let reading = single.measure_single_shot().expect("a reading");
109        assert_eq!(reading.co2_ppm, 3_000);
110        assert!((reading.celsius() + 5.0).abs() < 0.003);
111    }
112}