sensor_scd30/
lib.rs

1//! Scd30 driver library
2//!
3//! Copyright 2019 Ryan Kurte
4//!
5//! ``` no_run
6//! use std::time::Duration;
7//! use linux_embedded_hal::{I2cdev, Delay};
8//! use sensor_scd30::Scd30;
9//!
10//! // Open I2C port
11//! let i2c = I2cdev::new("/dev/i2c-1").unwrap();
12//!
13//! // Connect to sensor
14//! let mut scd = Scd30::new(i2c, Delay{}).unwrap();
15//!
16//! // Start continuous sampling mode
17//! scd.start_continuous(10).unwrap();
18//!
19//! // Poll for data
20//! loop {
21//!     // Keep looping until ready
22//!     if scd.data_ready().unwrap() {
23//!         continue;
24//!     }
25//!
26//!     // Fetch data when available
27//!     let m = scd.read_data().unwrap();
28//!     println!("Measurement: {:?}", m);
29//! }
30//!
31//!
32//! ```
33
34#![no_std]
35
36use core::fmt::Debug;
37use core::marker::PhantomData;
38
39use embedded_hal::delay::DelayNs;
40
41pub mod device;
42use device::*;
43
44pub mod base;
45use base::*;
46
47/// Scd30 sensor object
48/// This is generic over an I2C connector and associated error type
49pub struct Scd30<Conn, Delay, Err> {
50    conn: Conn,
51    delay: Delay,
52    _err: PhantomData<Err>,
53}
54
55/// Scd30 error object
56#[derive(Debug)]
57pub enum Error<ConnErr> {
58    Conn(ConnErr),
59    Crc(u8, u8),
60    NoDevice,
61}
62
63impl<ConnErr> From<ConnErr> for Error<ConnErr> {
64    fn from(conn_err: ConnErr) -> Self {
65        Error::Conn(conn_err)
66    }
67}
68
69/// Scd30 measurement object
70#[derive(PartialEq, Clone, Debug)]
71pub struct Measurement {
72    /// CO2 concentration in parts-per-million (PPM)
73    /// Range: 0 - 10,000
74    pub co2: f32,
75    /// Temperature in degrees celsius
76    /// Range: -40 - 125 C
77    pub temp: f32,
78    /// Relative Humidity (%)
79    /// Range: 0 - 100
80    pub rh: f32,
81}
82
83impl<Conn, Delay, Err> Scd30<Conn, Delay, Err>
84where
85    Conn: Base<Err>,
86    Delay: DelayNs,
87    Err: Debug,
88{
89    /// Create a new Scd30 sensor instance
90    pub fn new(conn: Conn, delay: Delay) -> Result<Self, Error<Err>> {
91        // Create sensor object
92        let mut s = Scd30 {
93            conn,
94            delay,
95            _err: PhantomData,
96        };
97
98        // Check communication
99        let v = s.firmware_version()?;
100        if v == 0x00 || v == 0xFF {
101            return Err(Error::NoDevice);
102        }
103
104        // Return sensor
105        Ok(s)
106    }
107
108    /// Start continuous sensing mode with optional pressure compensation
109    /// pressure_compensation should either be the current pressure in millibar or 0 to disable compensation
110    pub fn start_continuous(&mut self, pressure_compensation: u16) -> Result<(), Error<Err>> {
111        self.conn
112            .write_command(Command::StartContinuousMode, Some(pressure_compensation))
113    }
114
115    /// Stop continuous sensing mode
116    pub fn stop_continuous(&mut self) -> Result<(), Error<Err>> {
117        self.conn.write_command(Command::StopContinuousMode, None)
118    }
119
120    /// Configure measurement interval in seconds
121    pub fn set_measurement_interval(&mut self, interval: u16) -> Result<(), Error<Err>> {
122        self.conn
123            .write_command(Command::SetMeasurementInterval, Some(interval))
124    }
125
126    /// Enable or disable Automatic Self-Calibration
127    pub fn set_afc(&mut self, enabled: bool) -> Result<(), Error<Err>> {
128        let v = match enabled {
129            true => 1,
130            false => 0,
131        };
132
133        self.conn.write_command(Command::SetAfc, Some(v))
134    }
135
136    /// Set Forced Recalibration Value
137    /// This allows the sensor to be recalibrates using a reference CO2 source
138    pub fn set_frc(&mut self, cal_ppm: u16) -> Result<(), Error<Err>> {
139        self.conn.write_command(Command::SetFrc, Some(cal_ppm))
140    }
141
142    /// Set Temperature Compensation
143    /// Allows compensation for temperature variation during operation
144    pub fn set_temp_offset(&mut self, temperature: f32) -> Result<(), Error<Err>> {
145        let temperature = (temperature as u16) * 100;
146        self.conn
147            .write_command(Command::SetTempOffset, Some(temperature))
148    }
149
150    /// Set Altitude Compensation
151    /// Allows compensation for CO2 measurement using altitude over sea level
152    pub fn set_alt_offset(&mut self, altitude: u16) -> Result<(), Error<Err>> {
153        self.conn.write_command(Command::SetAltComp, Some(altitude))
154    }
155
156    /// Soft reset the underlying device
157    pub fn soft_reset(&mut self) -> Result<(), Error<Err>> {
158        self.conn.write_command(Command::SoftReset, None)
159    }
160
161    /// Fetch the device firmware version
162    pub fn firmware_version(&mut self) -> Result<u16, Error<Err>> {
163        let mut buff = [0u8; 3];
164
165        self.conn
166            .read_command(Command::GetFirmwareVersion, &mut buff)?;
167
168        let crc = crc8(&buff[..2]);
169        if crc != buff[2] {
170            return Err(Error::Crc(crc, buff[2]));
171        }
172
173        let v: u16 = (buff[0] as u16) << 8 | (buff[1] as u16);
174
175        Ok(v)
176    }
177
178    /// Check whether measurement data is available in the buffer
179    pub fn data_ready(&mut self) -> Result<bool, Error<Err>> {
180        let mut buff = [0u8; 3];
181
182        self.conn.read_command(Command::GetDataReady, &mut buff)?;
183
184        let crc = crc8(&buff[..2]);
185        if crc != buff[2] {
186            return Err(Error::Crc(crc, buff[2]));
187        }
188
189        Ok(buff[1] != 0)
190    }
191
192    /// Read measurement data from the buffer
193    pub fn read_data(&mut self) -> Result<Measurement, Error<Err>> {
194        let mut buff = [0u8; 18];
195
196        self.conn
197            .read_command(Command::ReadMeasurement, &mut buff)?;
198
199        let co2 = convert(&buff[0..6])?;
200        let temp = convert(&buff[6..12])?;
201        let rh = convert(&buff[12..18])?;
202
203        Ok(Measurement { co2, temp, rh })
204    }
205}
206
207/// Convert from a 6-byte response line into an F32 value
208fn convert<E>(line: &[u8]) -> Result<f32, Error<E>> {
209    // Lines MUST be 6 bytes long (MMSB, MLSB, CRC, LMSB, LLSB, CRC)
210    assert_eq!(line.len(), 6);
211
212    // Check CRC
213    let crc1 = crc8(&line[0..2]);
214    if crc1 != line[2] {
215        return Err(Error::Crc(crc1, line[2]));
216    }
217
218    let crc2 = crc8(&line[3..5]);
219    if crc2 != line[5] {
220        return Err(Error::Crc(crc2, line[5]));
221    }
222
223    // Build temporary u32
224    // Note the returned data is _big endian_
225    let u: u32 = ((line[0] as u32) << 24)
226        | ((line[1] as u32) << 16)
227        | ((line[3] as u32) << 8)
228        | ((line[4] as u32) << 0);
229
230    // Transmute into float
231    let v = f32::from_bits(u);
232
233    Ok(v)
234}
235
236#[cfg(test)]
237mod test {
238    extern crate std;
239    use std::vec;
240
241    use embedded_hal_mock::eh1::delay::NoopDelay;
242    use embedded_hal_mock::eh1::i2c::{Mock as I2cMock, Transaction as I2cTransaction};
243    use embedded_hal_mock::eh1::MockError;
244
245    use assert_approx_eq::assert_approx_eq;
246
247    use super::*;
248
249    #[test]
250    fn test_start_continuous() {
251        // Set up expectations
252        let expectations = [I2cTransaction::write(
253            DEFAULT_ADDRESS,
254            vec![0x00, 0x10, 0x00, 0x00, 0x81],
255        )];
256        let mut i2c = I2cMock::new(&expectations);
257
258        // Create sensor object
259        let mut sensor = Scd30 {
260            conn: i2c.clone(),
261            delay: NoopDelay {},
262            _err: PhantomData,
263        };
264
265        // Start continuous mode
266        sensor.start_continuous(0).unwrap();
267
268        // Finalize expectations
269        i2c.done();
270    }
271
272    #[test]
273    fn test_stop_continuous() {
274        // Set up expectations
275        let expectations = [I2cTransaction::write(DEFAULT_ADDRESS, vec![0x01, 0x04])];
276        let mut i2c = I2cMock::new(&expectations);
277
278        // Create sensor object
279        let mut sensor = Scd30 {
280            conn: i2c.clone(),
281            delay: NoopDelay {},
282            _err: PhantomData,
283        };
284
285        // Stop continuous mode
286        sensor.stop_continuous().unwrap();
287
288        // Finalize expectations
289        i2c.done();
290    }
291
292    #[test]
293    fn test_set_measurement_interval() {
294        // Set up expectations
295        let expectations = [I2cTransaction::write(
296            DEFAULT_ADDRESS,
297            vec![0x46, 0x00, 0x00, 0x02, 0xE3],
298        )];
299        let mut i2c = I2cMock::new(&expectations);
300
301        // Create sensor object
302        let mut sensor = Scd30 {
303            conn: i2c.clone(),
304            delay: NoopDelay {},
305            _err: PhantomData,
306        };
307
308        // Set measurement interval to 2s
309        sensor.set_measurement_interval(2).unwrap();
310
311        // Finalize expectations
312        i2c.done();
313    }
314
315    #[test]
316    fn test_set_frc() {
317        // Set up expectations
318        let expectations = [I2cTransaction::write(
319            DEFAULT_ADDRESS,
320            vec![0x52, 0x04, 0x01, 0xc2, 0x50],
321        )];
322        let mut i2c = I2cMock::new(&expectations);
323
324        // Create sensor object
325        let mut sensor = Scd30 {
326            conn: i2c.clone(),
327            delay: NoopDelay {},
328            _err: PhantomData,
329        };
330
331        // Set forced recalibration to 450ppm
332        sensor.set_frc(450).unwrap();
333
334        // Finalize expectations
335        i2c.done();
336    }
337
338    #[test]
339    fn set_temp_offset() {
340        // Set up expectations
341        let expectations = [I2cTransaction::write(
342            DEFAULT_ADDRESS,
343            vec![0x54, 0x03, 0x01, 0xF4, 0x33],
344        )];
345        let mut i2c = I2cMock::new(&expectations);
346
347        // Create sensor object
348        let mut sensor = Scd30 {
349            conn: i2c.clone(),
350            delay: NoopDelay {},
351            _err: PhantomData,
352        };
353
354        // Set temperature to 5 degrees
355        sensor.set_temp_offset(5.0).unwrap();
356
357        // Finalize expectations
358        i2c.done();
359    }
360
361    #[test]
362    fn set_alt_offset() {
363        // Set up expectations
364        let expectations = [I2cTransaction::write(
365            DEFAULT_ADDRESS,
366            vec![0x51, 0x02, 0x03, 0xE8, 0xD4],
367        )];
368        let mut i2c = I2cMock::new(&expectations);
369
370        // Create sensor object
371        let mut sensor = Scd30 {
372            conn: i2c.clone(),
373            delay: NoopDelay {},
374            _err: PhantomData,
375        };
376
377        // Set altitude to 1000m
378        sensor.set_alt_offset(1000).unwrap();
379
380        // Finalize expectations
381        i2c.done();
382    }
383
384    #[test]
385    fn test_soft_reset() {
386        // Set up expectations
387        let expectations = [I2cTransaction::write(DEFAULT_ADDRESS, vec![0xD3, 0x04])];
388        let mut i2c = I2cMock::new(&expectations);
389
390        // Create sensor object
391        let mut sensor = Scd30 {
392            conn: i2c.clone(),
393            delay: NoopDelay {},
394            _err: PhantomData,
395        };
396
397        // Signal for soft reset
398        sensor.soft_reset().unwrap();
399
400        // Finalize expectations
401        i2c.done();
402    }
403
404    #[test]
405    fn test_read_data_ready() {
406        // Set up expectations
407        let expectations = [
408            I2cTransaction::write(DEFAULT_ADDRESS, vec![0x02, 0x02]),
409            I2cTransaction::read(DEFAULT_ADDRESS | I2C_READ_FLAG, vec![0x00, 0x01, 0xB0]),
410        ];
411        let mut i2c = I2cMock::new(&expectations);
412
413        // Create sensor object
414        let mut sensor = Scd30 {
415            conn: i2c.clone(),
416            delay: NoopDelay {},
417            _err: PhantomData,
418        };
419
420        // Read data ready
421        let ready = sensor.data_ready().unwrap();
422        assert!(ready);
423
424        // Finalize expectations
425        i2c.done();
426    }
427
428    #[test]
429    fn test_read_measurement() {
430        // Set up expectations
431        let expectations = [
432            I2cTransaction::write(DEFAULT_ADDRESS, vec![0x03, 0x00]),
433            I2cTransaction::read(
434                DEFAULT_ADDRESS | I2C_READ_FLAG,
435                vec![
436                    0x43, 0xDB, 0xCB, 0x8C, 0x2E, 0x8F, // CO2: 439 ppm
437                    0x41, 0xD9, 0x70, 0xE7, 0xFF, 0xF5, // Temperature: 27.2 C
438                    0x42, 0x43, 0xBF, 0x3A, 0x1B, 0x74, // Relative humidity, 48.8 %
439                ],
440            ),
441        ];
442        let mut i2c = I2cMock::new(&expectations);
443
444        // Create sensor object
445        let mut sensor = Scd30 {
446            conn: i2c.clone(),
447            delay: NoopDelay {},
448            _err: PhantomData,
449        };
450
451        // Read measurement
452        let m = sensor.read_data().unwrap();
453
454        assert_approx_eq!(m.co2, 439.0, 0.1);
455        assert_approx_eq!(m.temp, 27.2, 0.1);
456        assert_approx_eq!(m.rh, 48.8, 0.1);
457
458        // Finalize expectations
459        i2c.done();
460    }
461
462    #[test]
463    fn test_convert() {
464        // Test vectors from datasheet
465        let tests = &[
466            ([0x43, 0xDB, 0xCB, 0x8C, 0x2E, 0x8F], 439.0),
467            ([0x41, 0xD9, 0x70, 0xE7, 0xFF, 0xF5], 27.2),
468            ([0x42, 0x43, 0xBF, 0x3A, 0x1B, 0x74], 48.8),
469        ];
470
471        for t in tests {
472            let v = convert::<()>(&t.0).unwrap();
473            assert_approx_eq!(v, t.1, 0.1);
474        }
475    }
476}