sgpc3/
lib.rs

1//! Platform agnostic Rust driver for Sensirion SGPC3 gas sensor using
2//! the [`embedded-hal`](https://github.com/japaric/embedded-hal) traits.
3//!
4//! ## Sensirion SGPC3
5//!
6//! Sensirion SGPC3 is a low-power accurate gas sensor for air quality application.
7//! The sensor has different sampling rates to optimize power-consumption per application
8//! bases as well as ability save and set the baseline for faster start-up accuracy.
9//! The sensor uses I²C interface and measures TVOC (*Total Volatile Organic Compounds*)
10//!
11//! Datasheet: <https://www.sensirion.com/file/datasheet_sgpc3>
12//!
13//! ## Usage
14//!
15//! ### Instantiating
16//!
17//! Import this crate and an `embedded_hal` implementation, then instantiate
18//! the device:
19//!
20//! ```ignore
21//! use linux_embedded_hal as hal;
22//!
23//! use hal::{Delay, I2cdev};
24//! use sgpc3::Sgpc3;
25//!
26//! let dev = I2cdev::new("/dev/i2c-1").unwrap();
27//! let mut sgp = Sgpc3::new(dev, 0x58, Delay);
28//!
29//! ```
30//!
31//! ### Fetching Sensor Feature Set
32//!
33//! Sensor feature set is important to determine the device capabilities.
34//! Most new sensors are at level 6 or above. Consult the datasheet for the implications.
35//!
36//! ```ignore
37//! use hal::{Delay, I2cdev};
38//! use sgpc3::Sgpc3;
39//!
40//!
41//! let dev = I2cdev::new("/dev/i2c-1").unwrap();
42//! let mut sensor = Sgpc3::new(dev, 0x58, Delay);
43//! let feature_set = sensor.get_feature_set().unwrap();
44//! println!("Feature set {:?}", feature_set);
45//! ```
46//!
47//! ### Doing Measurements
48//!
49//! Before you do any measurements, you need to initialize the sensor.
50//!
51//! ```ignore
52//!
53//! let dev = I2cdev::new("/dev/i2c-1").unwrap();
54//! let mut sensor = Sgpc3::new(dev, 0x58, Delay);
55//! sensor.init_preheat().unwrap();
56//!
57//! thread::sleep(Duration::new(16_u64, 0));
58//!
59//! loop {
60//!     let tvoc = sensor.measure_tvoc().unwrap();
61//!     println!("TVOC {}", tvoc);
62//! }
63
64//! ```
65//!
66//! SGPC3 has few things that you need to keep in mind. The first is pre-heating.
67//! The amount of preheating depends on when the sensor was used the last time and
68//! if you have the baseline saved. Baseline is adjusted inside the sensor with each measurement
69//! and you want to save it eg. each hour in the case you need to reset the sensor and start
70//! from the beginning.
71//!
72//! The recommended initialization flow is:
73//! ```no_run,ignore
74//! let dev = I2cdev::new("/dev/i2c-1");
75//! let mut sensor = Sgpc3::new(dev, 0x58, Delay);
76//! sensor.init_preheat();
77//! sensor.set_baseline(baseline);
78//!
79//! thread::sleep(Duration::new(sleep_time, 0));
80//! sensor.measure_tvoc();
81//! ```
82//!
83//! The table provides pre-heating times per sensor down-time
84//!
85//! | Sensor down-time                     | Accelerated warm-up time |
86//! |--------------------------------------|--------------------------|
87//! |            1 min – 30 min            |             -            |
88//! |             30 min – 6 h             |           16 s           |
89//! |             6 h – 1 week             |           184 s          |
90//! | more than 1 week / initial switch-on |           184 s          |
91//!
92//! Once the sensor has been taking the sufficient time for pre-heating. You need to call
93//! measurement function eg. 'measure_tvoc'. This will enable the sensor to go to sleep and continue initialization.
94//! The sensor readings won't change for the first 20s so you might as well discard all of them.
95//!
96//! You have two operation modes to choose from. Either the standard mode where you read the value
97//! every 2s or use the ultra-low power mode where you read the sensor every 30s.
98//!
99//! If you want to use ultra-power mode, you want to call that prior calling any init-function.
100//! Your baseline is power-mode dependent so you don't want to switch back and forth between
101//! the power-modes as it always requires re-initialization. Also, the sensor accuracy varies between
102//! the modes making the comparison of the values between modes no apples-to-apples anymore.
103//! In another words, choose your power-mode per your application and stick with it.
104//!
105//! After initialization, you are ready to measure TVOC in loop per the selected measurement interval.
106//! As said earlier, you want to stick with the internal - no shorter or longer than the defined value.
107//!
108//! If no stored baseline is available after initializing the baseline algorithm, the sensor has to run for
109//! 12 hours until the baseline can be stored. This will ensure an optimal behavior for subsequent startups.
110//! Reading out the baseline prior should be avoided unless a valid baseline is restored first. Once the
111//! baseline is properly initialized or restored, the current baseline value should be stored approximately
112//! once per hour. While the sensor is off, baseline values are valid for a maximum of seven days.
113//!
114//! SGPC3 is a great sensor and fun to use! I hope your sensor selection and this driver servers you well.
115#![cfg_attr(not(test), no_std)]
116
117use embedded_hal as hal;
118
119use hal::blocking::delay::DelayMs;
120use hal::blocking::i2c::{Read, Write, WriteRead};
121
122use sensirion_i2c::{crc8, i2c};
123
124const SGPC3_PRODUCT_TYPE: u8 = 1;
125const SGPC3_CMD_MEASURE_TEST_OK: u16 = 0xd400;
126
127/// Sgpc3 errors
128#[derive(Debug)]
129pub enum Error<E> {
130    /// I²C bus error
131    I2c(E),
132    /// CRC checksum validation failed
133    Crc,
134    ///Self-test measure failure
135    SelfTest,
136}
137
138impl<E, I2cWrite, I2cRead> From<i2c::Error<I2cWrite, I2cRead>> for Error<E>
139where
140    I2cWrite: Write<Error = E>,
141    I2cRead: Read<Error = E>,
142{
143    fn from(err: i2c::Error<I2cWrite, I2cRead>) -> Self {
144        match err {
145            i2c::Error::Crc => Error::Crc,
146            i2c::Error::I2cWrite(e) => Error::I2c(e),
147            i2c::Error::I2cRead(e) => Error::I2c(e),
148        }
149    }
150}
151
152#[derive(Debug, Copy, Clone)]
153enum Command {
154    /// Return the serial number.
155    GetSerial,
156    /// Acquires the supported feature set.
157    GetFeatureSet,
158    /// Run an on-chip self-test.
159    SelfTest,
160    /// Initialize 0 air quality measurements.
161    InitAirQuality0,
162    /// Initialize 64 air quality measurements.
163    InitAirQuality64,
164    /// Initialize continuous air quality measurements.
165    InitAirQualityContinuous,
166    /// Get a current air quality measurement.
167    MeasureAirQuality,
168    /// Measure raw signal.
169    MeasureRaw,
170    /// Return the baseline value.
171    GetAirQualityBaseline,
172    /// Return inceptive baseline value.
173    GetAirQualityInceptiveBaseline,
174    /// Measure air quality raw
175    MeasureAirQualityRaw,
176    /// Set the baseline value.
177    SetBaseline,
178    /// Set the current absolute humidity.
179    SetHumidity,
180    /// Setting power mode.
181    SetPowerMode,
182}
183
184impl Command {
185    /// Command and the requested delay in ms
186    fn as_tuple(self) -> (u16, u32) {
187        match self {
188            Command::GetSerial => (0x3682, 1), // This could have been 0.5ms
189            Command::GetFeatureSet => (0x202f, 1),
190            Command::SelfTest => (0x2032, 220),
191            Command::InitAirQuality0 => (0x2089, 10),
192            Command::InitAirQuality64 => (0x2003, 10),
193            Command::InitAirQualityContinuous => (0x20ae, 10),
194            Command::MeasureAirQuality => (0x2008, 50),
195            Command::MeasureRaw => (0x204d, 50),
196            Command::GetAirQualityBaseline => (0x2015, 10),
197            Command::GetAirQualityInceptiveBaseline => (0x20b3, 10),
198            Command::MeasureAirQualityRaw => (0x2046, 50),
199            Command::SetBaseline => (0x201e, 10),
200            Command::SetHumidity => (0x2061, 10),
201            Command::SetPowerMode => (0x209f, 10),
202        }
203    }
204}
205
206#[derive(Debug)]
207pub struct FeatureSet {
208    /// Product type for SGPC3 is always 1.
209    pub product_type: u8,
210    /// Product feature set defines the capabilities of the sensor. Consult datasheet
211    /// for the differences as they do impact on how you want to use APIs.
212    pub product_featureset: u8,
213}
214
215/// Calculated absolute humidity from relative humidity and temperature
216///
217/// Sensor is using mass of water vapor in given space as the means capture humidity
218/// and this can be calculated from relative humidity %.
219///
220/// Humidity (t_rh) and temperature (t_mc) are both expressed in kilounits (10C is 10000)
221/// Return value is in g/m^3 kilounits
222fn calculate_absolute_humidity(t_rh: i32, t_mc: i32) -> u32 {
223    type FP = fixed::types::I16F16;
224
225    // Rounding the last digit off as the sensors don't reach that level of accurary
226    // anyway
227    let t = FP::from_num(t_mc / 10) / 100; //    (t_mc as f32) / 1000_f32;
228    let rh = FP::from_num(t_rh / 10); // (rh as f32) / 1000_f32;
229
230    // Formulate for absolute humidy:
231    // rho_v = 216.7*(RH/100.0*6.112*exp(17.62*T/(243.12+T))/(273.15+T));
232
233    // Calculate the constants into one number
234    // 216.7 * 6.112 / 10,000 (10*1000)
235    let prefix_constants = FP::from_bits(0x21e8); // 0.13244704
236
237    let k = FP::from_bits(0x1112666); // 273.15
238    let m = FP::from_bits(0x119eb8); // 17.62
239    let t_n = FP::from_bits(0xf335c2); // 243.21
240
241    let temp_components = cordic::exp(m * t / (t_n + t));
242
243    let abs_hum = prefix_constants * rh * temp_components / (k + t);
244
245    (abs_hum * 1000).to_num::<u32>()
246}
247
248#[derive(Debug, Default)]
249pub struct Sgpc3<I2C, D> {
250    i2c: I2C,
251    address: u8,
252    delay: D,
253}
254
255impl<I2C, D, E> Sgpc3<I2C, D>
256where
257    I2C: Read<Error = E> + Write<Error = E> + WriteRead<Error = E>,
258    D: DelayMs<u32>,
259{
260    pub fn new(i2c: I2C, address: u8, delay: D) -> Self {
261        Sgpc3 {
262            i2c,
263            address,
264            delay,
265        }
266    }
267
268    /// Acquires the sensor serial number.
269    ///
270    /// Sensor serial number is only 48-bits long so the remaining 16-bits are zeros.
271    pub fn serial(&mut self) -> Result<u64, Error<E>> {
272        let mut serial = [0; 9];
273
274        self.delayed_read_cmd(Command::GetSerial, &mut serial)?;
275
276        let serial = u64::from(serial[0]) << 40
277            | u64::from(serial[1]) << 32
278            | u64::from(serial[3]) << 24
279            | u64::from(serial[4]) << 16
280            | u64::from(serial[6]) << 8
281            | u64::from(serial[7]);
282        Ok(serial)
283    }
284
285    /// Gets the sensor product type and supported feature set.
286    ///
287    /// The sensor uses feature versioning system to indicate the device capabilities.
288    /// Feature set 5 enables getting TVOC inceptive baseline.
289    /// Feature set 6 and above enables ultra-low power-save, setting absolute humidity and preheating.
290    /// The behaviour is undefined when using these functions with sensor not supporting the specific features.
291    pub fn get_feature_set(&mut self) -> Result<FeatureSet, Error<E>> {
292        let mut data = [0; 6];
293
294        self.delayed_read_cmd(Command::GetFeatureSet, &mut data)?;
295
296        let product_type = data[0] >> 4;
297
298        // This is great way to check if the integration and connection is working to sensor.
299        assert!(product_type == SGPC3_PRODUCT_TYPE);
300
301        Ok(FeatureSet {
302            product_type,
303            product_featureset: data[1],
304        })
305    }
306
307    /// Sets sensor into ultra-low power mode.
308    ///
309    /// The SGPC3 offers two operation modes with different power consumptions and sampling intervals. The low-power mode with
310    /// 1mA average current and 2s sampling interval and the ultra-low power mode with 0.065mA average current and 30s sampling
311    /// interval. By default, the SGPC3 is using the low-power mode. You want to stick with the sensor sampling internal so
312    /// you want to take the samples per the internal. The current SW implementation sees ultra low-power mode as
313    /// one-way street and once entered, one can get only get out of it through resetting the sensor.
314    #[inline]
315    pub fn set_ultra_power_mode(&mut self) -> Result<(), Error<E>> {
316        let power_mode: [u8; 2] = [0; 2];
317
318        self.write_command_with_args(Command::SetPowerMode, &power_mode)
319    }
320
321    /// Sensor self-test.
322    ///
323    /// Performs sensor self-test. This is intended for production line and testing and verification only and
324    /// shouldn't be needed for normal use. It should not be used after having issues any init commands.
325    pub fn self_test(&mut self) -> Result<&mut Self, Error<E>> {
326        let mut data = [0; 3];
327        self.delayed_read_cmd(Command::SelfTest, &mut data)?;
328
329        let result = u16::from_be_bytes([data[0], data[1]]);
330
331        if result != SGPC3_CMD_MEASURE_TEST_OK {
332            Err(Error::SelfTest)
333        } else {
334            Ok(self)
335        }
336    }
337
338    /// Command for reading values from the sensor
339    fn delayed_read_cmd(&mut self, cmd: Command, data: &mut [u8]) -> Result<(), Error<E>> {
340        self.write_command(cmd)?;
341        i2c::read_words_with_crc(&mut self.i2c, self.address, data)?;
342        Ok(())
343    }
344
345    /// Writes commands with arguments
346    fn write_command_with_args(&mut self, cmd: Command, data: &[u8]) -> Result<(), Error<E>> {
347        const MAX_TX_BUFFER: usize = 8;
348
349        let mut transfer_buffer = [0; MAX_TX_BUFFER];
350
351        let size = data.len();
352
353        // 2 for command, size of transferred bytes and CRC per each two bytes.
354        assert!(size < 2 + size + size / 2);
355        let (command, delay) = cmd.as_tuple();
356
357        transfer_buffer[0..2].copy_from_slice(&command.to_be_bytes());
358        let slice = &data[..2];
359        transfer_buffer[2..4].copy_from_slice(slice);
360        transfer_buffer[4] = crc8::calculate(slice);
361
362        let transfer_buffer = if size > 2 {
363            let slice = &data[2..4];
364            transfer_buffer[5..7].copy_from_slice(slice);
365            transfer_buffer[7] = crc8::calculate(slice);
366            &transfer_buffer[..]
367        } else {
368            &transfer_buffer[0..5]
369        };
370
371        self.i2c
372            .write(self.address, transfer_buffer)
373            .map_err(Error::I2c)?;
374        self.delay.delay_ms(delay);
375
376        Ok(())
377    }
378
379    /// Writes commands without additional arguments.
380    fn write_command(&mut self, cmd: Command) -> Result<(), Error<E>> {
381        let (command, delay) = cmd.as_tuple();
382        i2c::write_command(&mut self.i2c, self.address, command).map_err(Error::I2c)?;
383        self.delay.delay_ms(delay);
384        Ok(())
385    }
386
387    /// Initializes the sensor without preheat.
388    ///
389    /// Initializing without preheat will lead the early samples to be inaccurate. It is the
390    /// responsibility of the caller to wait the sufficient preheat period.
391    #[inline]
392    pub fn init_no_preheat(&mut self) -> Result<&mut Self, Error<E>> {
393        self.write_command(Command::InitAirQuality0)?;
394        Ok(self)
395    }
396
397    /// Initializes the sensor with preheat.
398    ///
399    /// This is the standard way of initializing the system.
400    #[inline]
401    pub fn init_preheat(&mut self) -> Result<(), Error<E>> {
402        self.write_command(Command::InitAirQualityContinuous)
403    }
404
405    /// Initializes the sensor with preheat for feature set 5 sensors
406    ///
407    /// This is the standard way of initializing the systems with feature set 5 sensor firmware
408    #[inline]
409    pub fn init_preheat_64s_fs5(&mut self) -> Result<(), Error<E>> {
410        self.write_command(Command::InitAirQuality64)
411    }
412
413    /// Sets the absolute humidity for the best accuracy.
414    ///
415    /// The argument must be supplied at fixed-point 8.8bit format.
416    #[inline]
417    pub fn set_absolute_humidity(&mut self, abs_hum: u32) -> Result<&mut Self, Error<E>> {
418        assert!(abs_hum <= 256000);
419
420        // This is Sensirion approximation for performing fixed-point 8.8bit number conversion
421        let scaled = ((abs_hum * 16777) >> 16) as u16;
422
423        self.write_command_with_args(Command::SetHumidity, &scaled.to_be_bytes())?;
424        Ok(self)
425    }
426
427    /// Sets the relative humidity for the best accuracy.
428    ///
429    /// The arguments are supplied as milli-units. Eg. 20% relative humidity is supplied as 20000
430    /// and temperature t_mc as Celsius. 10C is 10000.
431    #[inline]
432    pub fn set_relative_humidity(&mut self, rh: i32, t_mc: i32) -> Result<&mut Self, Error<E>> {
433        let abs_hum = calculate_absolute_humidity(rh, t_mc);
434
435        self.set_absolute_humidity(abs_hum as u32)
436    }
437
438    /// Measures both TVOC and RAW signal.
439    ///
440    /// The measurement should be performed at the configured sampling internal for the best accuracy.
441    /// The values are returned as tuple (TVOC, RAW)
442    pub fn measure_tvoc_and_raw(&mut self) -> Result<(u16, u16), Error<E>> {
443        let mut buffer = [0; 6];
444        self.delayed_read_cmd(Command::MeasureAirQualityRaw, &mut buffer)?;
445
446        let raw_signal = u16::from_be_bytes([buffer[0], buffer[1]]);
447        let tvoc_ppb = u16::from_be_bytes([buffer[3], buffer[4]]);
448        Ok((tvoc_ppb, raw_signal))
449    }
450
451    /// Measures TVOC
452    ///
453    /// The measurement should be performed at the configured sampling internal for the best accuracy.
454    pub fn measure_tvoc(&mut self) -> Result<u16, Error<E>> {
455        let mut buffer = [0; 3];
456        self.delayed_read_cmd(Command::MeasureAirQuality, &mut buffer)?;
457        let tvoc_ppb = u16::from_be_bytes([buffer[0], buffer[1]]);
458        Ok(tvoc_ppb)
459    }
460
461    /// Measures RAW signal
462    ///
463    /// The measurement should be performed at the configured sampling internal for the best accuracy.
464    /// Typically, the caller shouldn't need RAW value but should use TVOC instead.
465    pub fn measure_raw(&mut self) -> Result<u16, Error<E>> {
466        let mut buffer = [0; 3];
467        self.delayed_read_cmd(Command::MeasureRaw, &mut buffer)?;
468        let raw = u16::from_be_bytes([buffer[0], buffer[1]]);
469        Ok(raw)
470    }
471
472    /// Acquired the baseline for faster accurate sampling.
473    ///
474    /// Baseline can be used to reach faster accurate repeatable samples.
475    /// Sensor must be supporting feature set 6 for the support.
476    /// Check sensor application note for the usage as you need ensure that
477    /// sensor has been operating long-enough for valid baseline.
478    pub fn get_baseline(&mut self) -> Result<u16, Error<E>> {
479        let mut buffer = [0; 3];
480        self.delayed_read_cmd(Command::GetAirQualityBaseline, &mut buffer)?;
481        let baseline = u16::from_be_bytes([buffer[0], buffer[1]]);
482        Ok(baseline)
483    }
484
485    /// Acquired the inceptive baseline for faster accurate sampling.
486    ///
487    /// Baseline can be used to reach faster accurate repeatable samples.
488    /// This method needs to be used for sensors only supporting feature set 5 instead
489    /// of using get_tvoc_baseline.
490    ///
491    /// Check sensor application note for the usage as you need ensure that
492    /// sensor has been operating long-enough for valid baseline.
493    pub fn get_inceptive_baseline(&mut self) -> Result<u16, Error<E>> {
494        let mut buffer = [0; 3];
495        self.delayed_read_cmd(Command::GetAirQualityInceptiveBaseline, &mut buffer)?;
496        let baseline = u16::from_be_bytes([buffer[0], buffer[1]]);
497        Ok(baseline)
498    }
499
500    /// Sets the baseline for faster accurate.
501    ///
502    /// Baseline will ensure that you can start regarding the accuracy where you left it
503    /// off after powering down or reseting the sensor.
504    #[inline]
505    pub fn set_baseline(&mut self, baseline: u16) -> Result<&mut Self, Error<E>> {
506        self.write_command_with_args(Command::SetBaseline, &baseline.to_be_bytes())?;
507        Ok(self)
508    }
509
510    /// Initialize sensor for use
511    ///
512    /// Full initialization sequence for common way to initialize the sensor for production use.
513    /// This code uses the existing functionality making this shortcut to get things going for
514    /// those who don't want to learn the internal workings of the sensor. This method can only
515    /// be used with sensors supporting feature set 6 and above.
516    ///
517    /// It is assumed that ['baseline'] has been stored in system non-volatile memory with timestamp
518    /// during the earlier operation. Datasheet says "If no stored baseline is available after initializing
519    /// the baseline algorithm, the sensor has to run for 12 hours until the baseline can be stored.
520    /// This will ensure an optimal behavior for subsequent startups. Reading out the baseline prior should
521    /// be avoided unless a valid baseline is restored first. Once the baseline is properly initialized or
522    /// restored, the current baseline value should be stored approximately once per hour. While the sensor
523    /// is off, baseline values are valid for a maximum of seven days." Baseline age is provided in seconds
524    /// and set value zero if there is no baseline available.
525    ///
526    /// Initialization can take up to 204s so depending on the application the user may want to run this in own task.
527    ///
528    /// Once the method is complete, the user should immediately take a sample and then continue taking them
529    /// per the defined power-mode. In ultra power-save, the sampling frequency is 30s and in standard mode 2s.
530    pub fn initialize(
531        &mut self,
532        baseline: u16,
533        baseline_age_s: u32,
534        ultra_power_save: bool,
535    ) -> Result<&mut Self, Error<E>> {
536        if ultra_power_save {
537            self.set_ultra_power_mode()?;
538        }
539
540        self.init_preheat()?;
541
542        let sleep_time = if baseline_age_s == 0 || baseline_age_s > 7 * 24 * 60 * 60 {
543            // More than week old or initial switch-on
544            184 * 1000
545        } else {
546            self.set_baseline(baseline)?;
547
548            if baseline_age_s > 0 && baseline_age_s <= 30 * 60 {
549                // Less than 30min from the last save. This is fresh puppy
550                0
551            } else if baseline_age_s > 30 * 60 && baseline_age_s <= 6 * 60 * 60 {
552                // Less than six hours since the last baseline save
553                16 * 1000
554            } else {
555                // Maximum pre-head time but baseline still valid if less than week old
556                184 * 1000
557            }
558        };
559
560        self.delay.delay_ms(sleep_time);
561
562        // Releases preheat and start the internal sensor initialization
563        self.measure_tvoc()?;
564
565        // From the document: "After the accelerated warm-up phase, the initialization takes 20 seconds,
566        // during which the IAQ output will not change."
567        self.delay.delay_ms(20 * 1000);
568        Ok(self)
569    }
570}
571
572// Testing is focused on checking the primitive transactions. It is assumed that during
573// the real sensor testing, the basic flows in the command structure has been caught.
574#[cfg(test)]
575mod tests {
576    use embedded_hal_mock as hal;
577
578    use self::hal::delay::MockNoop as DelayMock;
579    use self::hal::i2c::{Mock as I2cMock, Transaction};
580    use super::*;
581
582    /// Test the `serial` function
583    #[test]
584    fn serial() {
585        let (cmd, _) = Command::GetSerial.as_tuple();
586        let expectations = [
587            Transaction::write(0x58, cmd.to_be_bytes().to_vec()),
588            Transaction::read(
589                0x58,
590                vec![0xde, 0xad, 0x98, 0xbe, 0xef, 0x92, 0xde, 0xad, 0x98],
591            ),
592        ];
593        let mock = I2cMock::new(&expectations);
594        let mut sensor = Sgpc3::new(mock, 0x58, DelayMock);
595        let serial = sensor.serial().unwrap();
596        assert_eq!(serial, 0x00deadbeefdead);
597    }
598
599    #[test]
600    fn selftest_ok() {
601        let (cmd, _) = Command::SelfTest.as_tuple();
602        let expectations = [
603            Transaction::write(0x58, cmd.to_be_bytes().to_vec()),
604            Transaction::read(0x58, vec![0xD4, 0x00, 0xC6]),
605        ];
606        let mock = I2cMock::new(&expectations);
607        let mut sensor = Sgpc3::new(mock, 0x58, DelayMock);
608        assert!(sensor.self_test().is_ok());
609    }
610
611    #[test]
612    fn selftest_failed() {
613        let (cmd, _) = Command::SelfTest.as_tuple();
614        let expectations = [
615            Transaction::write(0x58, cmd.to_be_bytes().to_vec()),
616            Transaction::read(0x58, vec![0xde, 0xad, 0x98]),
617        ];
618        let mock = I2cMock::new(&expectations);
619        let mut sensor = Sgpc3::new(mock, 0x58, DelayMock);
620        assert!(!sensor.self_test().is_ok());
621    }
622
623    #[test]
624    fn test_crc_error() {
625        let (cmd, _) = Command::SelfTest.as_tuple();
626        let expectations = [
627            Transaction::write(0x58, cmd.to_be_bytes().to_vec()),
628            Transaction::read(0x58, vec![0xD4, 0x00, 0x00]),
629        ];
630        let mock = I2cMock::new(&expectations);
631        let mut sensor = Sgpc3::new(mock, 0x58, DelayMock);
632
633        match sensor.self_test() {
634            Err(Error::Crc) => {}
635            Err(_) => panic!("Unexpected error in CRC test"),
636            Ok(_) => panic!("Unexpected success in CRC test"),
637        }
638    }
639
640    #[test]
641    fn measure_tvoc_and_raw() {
642        let (cmd, _) = Command::MeasureAirQualityRaw.as_tuple();
643        let expectations = [
644            Transaction::write(0x58, cmd.to_be_bytes().to_vec()),
645            Transaction::read(0x58, vec![0x12, 0x34, 0x37, 0xbe, 0xef, 0x92]),
646        ];
647        let mock = I2cMock::new(&expectations);
648        let mut sensor = Sgpc3::new(mock, 0x58, DelayMock);
649        let (tvoc, raw) = sensor.measure_tvoc_and_raw().unwrap();
650        assert_eq!(tvoc, 0xbeef);
651        assert_eq!(raw, 0x1234);
652    }
653
654    #[test]
655    fn absolute_humidity() {
656        let humidity = vec![
657            // temp, rh hum, absolute hum
658            (10_000, 25_000, 2359),
659            (10_000, 50_000, 4717),
660            (25_000, 25_000, 5782),
661            (25_000, 50_000, 11565),
662            (25_000, 75_000, 17348),
663        ];
664
665        for (i, (t, rh, abs_hum)) in humidity.iter().enumerate() {
666            let calc_abs_hum = calculate_absolute_humidity(*rh, *t) as i32;
667            let delta = if calc_abs_hum > *abs_hum {
668                calc_abs_hum - abs_hum
669            } else {
670                abs_hum - calc_abs_hum
671            };
672
673            assert!(
674                delta < 200,
675                "Calculated value = {}, Reference value = {} in index {}",
676                calc_abs_hum,
677                abs_hum,
678                i
679            );
680        }
681    }
682}