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}