Skip to main content

pamoja_sensors/scd4x/
driver.rs

1//! The SCD4x driven over I2C: identify, run periodic measurements, and fetch each one.
2
3use embedded_hal::delay::DelayNs;
4use embedded_hal::i2c::I2c;
5use pamoja_core::Sensor;
6
7use super::{
8    command, command_frame, data_ready, max_duration_ms, serial_number, temperature_offset_word,
9    word, write_frame, Measurement, I2C_ADDRESS,
10};
11use crate::error::DriverError;
12
13/// How long the driver waits between data-ready polls, in milliseconds.
14pub const DATA_READY_POLL_MILLIS: u32 = 100;
15
16/// How many data-ready polls the driver makes before a measurement is called
17/// overdue: a little more than one five-second signal update interval.
18pub const DATA_READY_POLLS: u8 = 60;
19
20/// An SCD40 or SCD41 on an I2C bus, running periodic measurements.
21///
22/// [`init`](Scd4x::init) stops any periodic measurement a previous run left going,
23/// reads the serial number, which is the identity a part without an id register
24/// offers, and starts periodic measurement, after which the part produces a new
25/// result every five seconds. [`measure`](Scd4x::measure) polls the data-ready
26/// status until a result waits, then reads it. Every command follows the
27/// datasheet's sequence rules: the two command bytes go out most significant first,
28/// the command's execution time passes before the read header is sent, and the CRC
29/// on every word read is checked. As a [`Sensor`] its reading is the
30/// [`Measurement`]; one channel is selected with [`Sensor::map`].
31///
32/// # Examples
33///
34/// A meeting room's air, read from an SCD4x that is not plugged in yet: the simulated part,
35/// which the `alloc` feature brings, holds 1,200 ppm of carbon dioxide at 22 °C and 40 %
36/// relative humidity.
37///
38/// ```
39/// # #[cfg(feature = "alloc")]
40/// # {
41/// use pamoja_core::Sensor;
42/// use pamoja_hal::script::{block_on, DelayLog};
43/// use pamoja_sensors::scd4x::{sim, Scd4x};
44///
45/// let mut sensor = Scd4x::new(sim::reporting(1_200, 22.0, 40.0), DelayLog::new());
46/// let air = block_on(sensor.read())?;
47/// assert_eq!(air.co2_ppm, 1_200);
48/// # }
49/// # Ok::<(), pamoja_core::Error>(())
50/// ```
51#[derive(Debug)]
52pub struct Scd4x<I2C, D> {
53    bus: I2C,
54    delay: D,
55    serial: Option<u64>,
56}
57
58impl<I2C, D> Scd4x<I2C, D> {
59    /// Wraps an I2C bus; the part has one address.
60    ///
61    /// # Arguments
62    ///
63    /// * `bus` - the I2C bus the part is on.
64    /// * `delay` - the timer that paces the commands and the polls.
65    ///
66    /// # Returns
67    ///
68    /// The driver.
69    pub fn new(bus: I2C, delay: D) -> Self {
70        Scd4x {
71            bus,
72            delay,
73            serial: None,
74        }
75    }
76
77    /// Returns the serial number read at initialization, if it has happened.
78    pub fn serial(&self) -> Option<u64> {
79        self.serial
80    }
81
82    /// Gives back the bus and the delay.
83    ///
84    /// # Returns
85    ///
86    /// The bus and delay the driver was built from.
87    pub fn release(self) -> (I2C, D) {
88        (self.bus, self.delay)
89    }
90}
91
92impl<I2C: I2c, D: DelayNs> Scd4x<I2C, D> {
93    /// Stops any running measurement, reads the serial number, and starts periodic
94    /// measurement.
95    ///
96    /// # Errors
97    ///
98    /// Returns [`DriverError::Bus`] if the bus fails, and [`DriverError::Sensor`]
99    /// with [`SensorError::Crc`](crate::SensorError::Crc) if the serial number
100    /// arrives corrupted.
101    pub fn init(&mut self) -> Result<(), DriverError<I2C::Error>> {
102        self.send(command::STOP_PERIODIC_MEASUREMENT)?;
103        self.send(command::GET_SERIAL_NUMBER)?;
104        let mut frame = [0u8; 9];
105        self.bus
106            .read(I2C_ADDRESS, &mut frame)
107            .map_err(DriverError::Bus)?;
108        let serial = serial_number(&frame)?;
109        self.send(command::START_PERIODIC_MEASUREMENT)?;
110        self.serial = Some(serial);
111        Ok(())
112    }
113
114    /// Waits for the next periodic result and reads it.
115    ///
116    /// Initializes the part first if [`init`](Scd4x::init) has not run. The
117    /// data-ready status is polled every [`DATA_READY_POLL_MILLIS`] up to
118    /// [`DATA_READY_POLLS`] times, a little over one signal update interval.
119    ///
120    /// # Returns
121    ///
122    /// The CO2, temperature, and humidity words as a [`Measurement`].
123    ///
124    /// # Errors
125    ///
126    /// Returns [`DriverError::Bus`] if the bus fails, [`DriverError::Timeout`] if
127    /// no result becomes ready, [`DriverError::Sensor`] with
128    /// [`SensorError::Crc`](crate::SensorError::Crc) if a word arrives corrupted, or
129    /// any error of [`init`](Scd4x::init).
130    pub fn measure(&mut self) -> Result<Measurement, DriverError<I2C::Error>> {
131        if self.serial.is_none() {
132            self.init()?;
133        }
134        let mut ready = false;
135        for _ in 0..DATA_READY_POLLS {
136            if self.data_ready()? {
137                ready = true;
138                break;
139            }
140            self.delay.delay_ms(DATA_READY_POLL_MILLIS);
141        }
142        if !ready {
143            return Err(DriverError::Timeout);
144        }
145        self.read_measurement()
146    }
147
148    /// Runs one on-demand measurement on an SCD41, which takes five seconds.
149    ///
150    /// The part must not be in periodic measurement: call [`stop`](Scd4x::stop)
151    /// first, or use this instead of [`init`](Scd4x::init) on a bus where the part
152    /// is idle.
153    ///
154    /// # Returns
155    ///
156    /// The measurement.
157    ///
158    /// # Errors
159    ///
160    /// Returns [`DriverError::Bus`] if the bus fails, and [`DriverError::Sensor`]
161    /// with [`SensorError::Crc`](crate::SensorError::Crc) if a word arrives
162    /// corrupted.
163    pub fn measure_single_shot(&mut self) -> Result<Measurement, DriverError<I2C::Error>> {
164        self.send(command::MEASURE_SINGLE_SHOT)?;
165        self.read_measurement()
166    }
167
168    /// Asks whether a periodic result is waiting.
169    ///
170    /// # Returns
171    ///
172    /// `true` when [`measure`](Scd4x::measure) would read without waiting.
173    ///
174    /// # Errors
175    ///
176    /// Returns [`DriverError::Bus`] if the bus fails, and [`DriverError::Sensor`]
177    /// with [`SensorError::Crc`](crate::SensorError::Crc) if the status word
178    /// arrives corrupted.
179    pub fn data_ready(&mut self) -> Result<bool, DriverError<I2C::Error>> {
180        self.send(command::GET_DATA_READY_STATUS)?;
181        let mut frame = [0u8; 3];
182        self.bus
183            .read(I2C_ADDRESS, &mut frame)
184            .map_err(DriverError::Bus)?;
185        Ok(data_ready(word(&frame)?))
186    }
187
188    /// Stops periodic measurement, after which the settings commands are allowed.
189    ///
190    /// # Errors
191    ///
192    /// Returns [`DriverError::Bus`] if the bus fails.
193    pub fn stop(&mut self) -> Result<(), DriverError<I2C::Error>> {
194        self.send(command::STOP_PERIODIC_MEASUREMENT)
195    }
196
197    /// Starts periodic measurement.
198    ///
199    /// # Errors
200    ///
201    /// Returns [`DriverError::Bus`] if the bus fails.
202    pub fn start(&mut self) -> Result<(), DriverError<I2C::Error>> {
203        self.send(command::START_PERIODIC_MEASUREMENT)
204    }
205
206    /// Sets the temperature offset that compensates the part's self-heating.
207    ///
208    /// Periodic measurement is stopped, the offset written, and measurement started
209    /// again, since the part only takes settings while idle. The setting lasts until
210    /// power is lost unless persisted by the part's own command.
211    ///
212    /// # Arguments
213    ///
214    /// * `milli_celsius` - the offset to subtract, in millidegrees Celsius.
215    ///
216    /// # Errors
217    ///
218    /// Returns [`DriverError::Bus`] if the bus fails.
219    pub fn set_temperature_offset(
220        &mut self,
221        milli_celsius: u32,
222    ) -> Result<(), DriverError<I2C::Error>> {
223        self.stop()?;
224        self.write(
225            command::SET_TEMPERATURE_OFFSET,
226            temperature_offset_word(milli_celsius),
227        )?;
228        self.start()
229    }
230
231    /// Sets the altitude the part corrects its CO2 reading for.
232    ///
233    /// Periodic measurement is stopped, the altitude written, and measurement
234    /// started again.
235    ///
236    /// # Arguments
237    ///
238    /// * `meters` - the altitude above sea level.
239    ///
240    /// # Errors
241    ///
242    /// Returns [`DriverError::Bus`] if the bus fails.
243    pub fn set_sensor_altitude(&mut self, meters: u16) -> Result<(), DriverError<I2C::Error>> {
244        self.stop()?;
245        self.write(command::SET_SENSOR_ALTITUDE, meters)?;
246        self.start()
247    }
248
249    fn read_measurement(&mut self) -> Result<Measurement, DriverError<I2C::Error>> {
250        self.send(command::READ_MEASUREMENT)?;
251        let mut frame = [0u8; 9];
252        self.bus
253            .read(I2C_ADDRESS, &mut frame)
254            .map_err(DriverError::Bus)?;
255        Ok(Measurement::parse(&frame)?)
256    }
257
258    fn send(&mut self, command: u16) -> Result<(), DriverError<I2C::Error>> {
259        self.bus
260            .write(I2C_ADDRESS, &command_frame(command))
261            .map_err(DriverError::Bus)?;
262        self.wait(command);
263        Ok(())
264    }
265
266    fn write(&mut self, command: u16, value: u16) -> Result<(), DriverError<I2C::Error>> {
267        self.bus
268            .write(I2C_ADDRESS, &write_frame(command, value))
269            .map_err(DriverError::Bus)?;
270        self.wait(command);
271        Ok(())
272    }
273
274    fn wait(&mut self, command: u16) {
275        if let Some(millis) = max_duration_ms(command) {
276            self.delay.delay_ms(u32::from(millis));
277        }
278    }
279}
280
281impl<I2C, D> Sensor for Scd4x<I2C, D>
282where
283    I2C: I2c + Send,
284    I2C::Error: core::fmt::Debug,
285    D: DelayNs + Send,
286{
287    type Reading = Measurement;
288
289    async fn read(&mut self) -> pamoja_core::Result<Measurement> {
290        self.measure().map_err(pamoja_core::Error::from)
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use crate::scd4x::{serial_number_frame, word_frame, PERIODIC_MEASUREMENT_INTERVAL_MS};
298    use crate::SensorError;
299    use alloc::vec;
300    use alloc::vec::Vec;
301    use embedded_hal::i2c::{ErrorKind, NoAcknowledgeSource};
302    use pamoja_hal::script::{block_on, DelayLog, I2cScript, I2cStep};
303
304    const SERIAL: u64 = 0x1234_5678_9ABC;
305
306    fn send(command: u16) -> I2cStep {
307        I2cStep::write(I2C_ADDRESS, command_frame(command))
308    }
309
310    fn init_steps() -> Vec<I2cStep> {
311        vec![
312            send(command::STOP_PERIODIC_MEASUREMENT),
313            send(command::GET_SERIAL_NUMBER),
314            I2cStep::read(I2C_ADDRESS, serial_number_frame(SERIAL)),
315            send(command::START_PERIODIC_MEASUREMENT),
316        ]
317    }
318
319    fn sample() -> Measurement {
320        Measurement::from_physical(500, 25_000, 37_000)
321    }
322
323    #[test]
324    fn init_stops_identifies_and_starts_then_measure_polls_and_reads() {
325        let mut steps = init_steps();
326        steps.extend([
327            send(command::GET_DATA_READY_STATUS),
328            I2cStep::read(I2C_ADDRESS, word_frame(0x8000)),
329            send(command::GET_DATA_READY_STATUS),
330            I2cStep::read(I2C_ADDRESS, word_frame(0x8006)),
331            send(command::READ_MEASUREMENT),
332            I2cStep::read(I2C_ADDRESS, sample().to_bytes()),
333        ]);
334        let mut sensor = Scd4x::new(I2cScript::new(steps), DelayLog::new());
335        sensor.init().unwrap();
336        assert_eq!(sensor.serial(), Some(SERIAL));
337        let measurement = sensor.measure().unwrap();
338        assert_eq!(measurement, sample());
339        assert_eq!(measurement.co2_ppm, 500);
340        let (bus, delay) = sensor.release();
341        assert!(bus.done());
342        assert_eq!(
343            delay.waits_ns(),
344            [500_000_000, 1_000_000, 1_000_000, 100_000_000, 1_000_000, 1_000_000],
345            "stop waits 500 ms, each read waits its 1 ms execution time, a not-ready poll waits 100 ms"
346        );
347    }
348
349    #[test]
350    fn a_result_that_never_comes_is_a_timeout() {
351        let mut steps = init_steps();
352        for _ in 0..DATA_READY_POLLS {
353            steps.push(send(command::GET_DATA_READY_STATUS));
354            steps.push(I2cStep::read(I2C_ADDRESS, word_frame(0x8000)));
355        }
356        let mut sensor = Scd4x::new(I2cScript::new(steps), DelayLog::new());
357        assert_eq!(sensor.measure(), Err(DriverError::Timeout));
358        let (bus, delay) = sensor.release();
359        assert!(bus.done());
360        let polls = u64::from(DATA_READY_POLLS);
361        assert_eq!(delay.total_millis(), 500 + 1 + polls * (1 + 100));
362        assert!(delay.total_millis() > u64::from(PERIODIC_MEASUREMENT_INTERVAL_MS));
363    }
364
365    #[test]
366    fn a_corrupted_word_is_a_crc_failure_and_a_bus_fault_a_bus_error() {
367        let mut corrupted = serial_number_frame(SERIAL);
368        corrupted[2] ^= 0x01;
369        let steps = [
370            send(command::STOP_PERIODIC_MEASUREMENT),
371            send(command::GET_SERIAL_NUMBER),
372            I2cStep::read(I2C_ADDRESS, corrupted),
373        ];
374        let mut sensor = Scd4x::new(I2cScript::new(steps), DelayLog::new());
375        assert_eq!(sensor.init(), Err(DriverError::Sensor(SensorError::Crc)));
376        assert_eq!(sensor.serial(), None);
377
378        let kind = ErrorKind::NoAcknowledge(NoAcknowledgeSource::Address);
379        let mut sensor = Scd4x::new(
380            I2cScript::new([I2cStep::fault(I2C_ADDRESS, kind)]),
381            DelayLog::new(),
382        );
383        assert!(matches!(sensor.init(), Err(DriverError::Bus(_))));
384    }
385
386    #[test]
387    fn a_single_shot_waits_five_seconds_then_reads() {
388        let steps = [
389            send(command::MEASURE_SINGLE_SHOT),
390            send(command::READ_MEASUREMENT),
391            I2cStep::read(I2C_ADDRESS, sample().to_bytes()),
392        ];
393        let mut sensor = Scd4x::new(I2cScript::new(steps), DelayLog::new());
394        assert_eq!(sensor.measure_single_shot().unwrap(), sample());
395        let (bus, delay) = sensor.release();
396        assert!(bus.done());
397        assert_eq!(delay.total_millis(), 5_001);
398    }
399
400    #[test]
401    fn settings_stop_write_and_restart_the_periodic_measurement() {
402        let mut steps = init_steps();
403        steps.extend([
404            send(command::STOP_PERIODIC_MEASUREMENT),
405            I2cStep::write(
406                I2C_ADDRESS,
407                write_frame(
408                    command::SET_TEMPERATURE_OFFSET,
409                    temperature_offset_word(4_000),
410                ),
411            ),
412            send(command::START_PERIODIC_MEASUREMENT),
413            send(command::STOP_PERIODIC_MEASUREMENT),
414            I2cStep::write(
415                I2C_ADDRESS,
416                write_frame(command::SET_SENSOR_ALTITUDE, 1_500),
417            ),
418            send(command::START_PERIODIC_MEASUREMENT),
419        ]);
420        let mut sensor = Scd4x::new(I2cScript::new(steps), DelayLog::new());
421        sensor.init().unwrap();
422        sensor.set_temperature_offset(4_000).unwrap();
423        sensor.set_sensor_altitude(1_500).unwrap();
424        assert!(sensor.release().0.done());
425    }
426
427    #[test]
428    fn as_a_sensor_it_reads_and_maps_to_co2() {
429        let mut steps = init_steps();
430        steps.extend([
431            send(command::GET_DATA_READY_STATUS),
432            I2cStep::read(I2C_ADDRESS, word_frame(0x8006)),
433            send(command::READ_MEASUREMENT),
434            I2cStep::read(I2C_ADDRESS, sample().to_bytes()),
435        ]);
436        let sensor = Scd4x::new(I2cScript::new(steps), DelayLog::new());
437        let mut co2 = sensor.map(|measurement| measurement.co2_ppm);
438        assert_eq!(block_on(co2.read()).unwrap(), 500);
439    }
440
441    #[test]
442    fn start_up_and_one_reading_make_the_transfers_the_datasheet_gives() {
443        use crate::scd4x::{Measurement, I2C_ADDRESS};
444
445        const PART: u8 = I2C_ADDRESS;
446        let bus = I2cScript::new([
447            I2cStep::write(PART, [0x3F, 0x86]),
448            I2cStep::write(PART, [0x36, 0x82]),
449            I2cStep::read(PART, serial_number_frame(0x1234_5678_9ABC)),
450            I2cStep::write(PART, [0x21, 0xB1]),
451            I2cStep::write(PART, [0xE4, 0xB8]),
452            I2cStep::read(PART, word_frame(0x8006)),
453            I2cStep::write(PART, [0xEC, 0x05]),
454            I2cStep::read(
455                PART,
456                Measurement::from_physical(500, 25_000, 37_000).to_bytes(),
457            ),
458        ]);
459
460        let mut sensor = Scd4x::new(bus, DelayLog::new());
461        let measurement = block_on(Sensor::read(&mut sensor)).expect("the scripted part answers");
462        assert_eq!(measurement.co2_ppm, 500);
463        assert_eq!(sensor.serial(), Some(0x1234_5678_9ABC));
464    }
465}