Skip to main content

embedded_sgp30/
lib.rs

1#![doc = include_str!("../README.md")]
2#![deny(unsafe_code, missing_docs)]
3#![no_std]
4
5use crc::{Crc, CRC_8_NRSC_5};
6#[allow(unused_imports)]
7use micromath::F32Ext;
8
9#[cfg(not(feature = "async"))]
10use embedded_hal as hal;
11#[cfg(feature = "async")]
12use embedded_hal_async as hal;
13
14use hal::i2c::{Operation, SevenBitAddress};
15
16/// The I2C address of the SGP30 chip
17pub const DEFAULT_I2C_ADDRESS: SevenBitAddress = 0x58;
18
19const GET_BASELINE_COMMAND: &[u8] = &[0x20, 0x15];
20const GET_FEATURE_SET_VERSION_COMMAND: &[u8] = &[0x20, 0x2f];
21const GET_SERIAL_ID_COMMAND: &[u8] = &[0x36, 0x82];
22const INIT_AIR_QUALITY_COMMAND: &[u8] = &[0x20, 0x03];
23const MEASURE_AIR_QUALITY_COMMAND: &[u8] = &[0x20, 0x08];
24const MEASURE_RAW_SIGNALS_COMMAND: &[u8] = &[0x20, 0x50];
25const RESET_COMMAND: &[u8] = &[0x00, 0x06];
26const SET_BASELINE_COMMAND: &[u8] = &[0x20, 0x1e];
27const SET_HUMIDITY_COMMAND: &[u8] = &[0x20, 0x61];
28
29/// All possible errors generated when using the Sgp30 struct
30#[derive(Debug)]
31pub enum Error<I2cE>
32where
33    I2cE: hal::i2c::Error,
34{
35    /// I²C bus error
36    I2c(I2cE),
37    /// The SGP30 chip has not been detected
38    ChipNotDetected,
39    /// The detected chip is an invalid product, either it has an invalid
40    /// product type, or an invalid product version
41    InvalidProduct,
42    /// The operation that is asked for is not supported by this version of
43    /// the sensor.
44    FeatureNotSupported,
45    /// The computed CRC and the one sent by the device mismatch
46    BadCrc,
47}
48
49impl<I2cE> From<I2cE> for Error<I2cE>
50where
51    I2cE: hal::i2c::Error,
52{
53    fn from(value: I2cE) -> Self {
54        Error::I2c(value)
55    }
56}
57
58/// The result of an air quality measurement.
59#[derive(Clone, Copy, Debug, Default)]
60pub struct AirQuality {
61    /// The value of the CO₂ equivalent signal (CO₂eq) in ppm (parts per
62    /// million).
63    pub co2: u16,
64    /// The value of the TVOC signal in ppb (parts per billion).
65    pub tvoc: u16,
66}
67
68/// The result of a raw signals measurement.
69#[derive(Clone, Copy, Debug, Default)]
70pub struct RawSignals {
71    /// The value of the ethanol signal in ppm (parts per million).
72    pub ethanol: u16,
73    /// The value of the H₂ signal in ppm (parts per million).
74    pub h2: u16,
75}
76
77/// SGP30 device driver
78#[derive(Debug)]
79pub struct Sgp30<I2C, D> {
80    address: SevenBitAddress,
81    delay: D,
82    i2c: I2C,
83    product_version: u8,
84}
85
86impl<I2C, D> Sgp30<I2C, D>
87where
88    I2C: hal::i2c::I2c,
89    D: hal::delay::DelayNs,
90{
91    /// Create a new instance of the SGP30 device.
92    #[maybe_async_cfg::maybe(
93        sync(not(feature = "async"), keep_self),
94        async(feature = "async", keep_self)
95    )]
96    pub async fn new(
97        i2c: I2C,
98        address: SevenBitAddress,
99        delay: D,
100    ) -> Result<Self, Error<I2C::Error>> {
101        let mut device = Self {
102            address,
103            delay,
104            i2c,
105            product_version: 0,
106        };
107
108        // Check that the chip is present
109        if device.get_serial_id().await.is_err() {
110            return Err(Error::ChipNotDetected);
111        }
112
113        // Check the feature set
114        let feature_set_version = device.get_feature_set_version().await?;
115        let product_type = (feature_set_version & 0xf000) >> 12;
116        let product_version = (feature_set_version & 0x00ff) as u8;
117        if product_type != 0 || product_version == 0 {
118            return Err(Error::InvalidProduct);
119        }
120        device.product_version = product_version;
121
122        Ok(device)
123    }
124
125    /// Get the baseline values for the air quality signals.
126    ///
127    /// The goal of this feature is to save the baseline at regular intervals
128    /// on an external non-volatile memory and be able to restore these values
129    /// after a new power-up or a soft reset of the sensor.
130    ///
131    /// See [`Sgp30::set_baseline()`] for instructions on how to restore the
132    /// values that have been saved here.
133    #[maybe_async_cfg::maybe(
134        sync(not(feature = "async"), keep_self),
135        async(feature = "async", keep_self)
136    )]
137    pub async fn get_baseline(&mut self) -> Result<AirQuality, Error<I2C::Error>> {
138        self.get_air_quality(GET_BASELINE_COMMAND, 10).await
139    }
140
141    /// Start the air quality measurement
142    ///
143    /// After this function has been called, the
144    /// [`Sgp30::measure_air_quality()`] function has to be called at regular
145    /// intervals of 1s to ensure the proper operation of the dynamic baseline
146    /// compensation algorithm.
147    /// This has to be called after every power-up or after each soft reset
148    /// performed with [`Sgp30::reset()`].
149    #[maybe_async_cfg::maybe(
150        sync(not(feature = "async"), keep_self),
151        async(feature = "async", keep_self)
152    )]
153    pub async fn initialize_air_quality_measure(&mut self) -> Result<(), Error<I2C::Error>> {
154        self.i2c
155            .write(self.address, INIT_AIR_QUALITY_COMMAND)
156            .await?;
157        self.delay.delay_ms(10).await;
158        Ok(())
159    }
160
161    /// Measure the air quality (CO₂eq and TVOC).
162    ///
163    /// This function has to be called at regular intervals of 1s after the air
164    /// quality measure has been initialized with
165    /// [`Sgp30::initialize_air_quality_measure()`], to ensure the proper
166    /// operation of the dynamic baseline compensation algorithm.
167    ///
168    /// For the first 15s after the initialization, the sensor is in an
169    /// initialization phase and this function will return an air quality
170    /// measure with fixed values of 400 ppm CO₂eq and 0 ppb TVOC.
171    #[maybe_async_cfg::maybe(
172        sync(not(feature = "async"), keep_self),
173        async(feature = "async", keep_self)
174    )]
175    pub async fn measure_air_quality(&mut self) -> Result<AirQuality, Error<I2C::Error>> {
176        self.get_air_quality(MEASURE_AIR_QUALITY_COMMAND, 12).await
177    }
178
179    /// Measure the raw signals (H₂ and ethanol).
180    ///
181    /// <div class="warning">This is intended for part verification and testing
182    /// purposes, therefore you should not need it.</div>
183    ///
184    /// It returns the raw signals of the sensor.
185    #[maybe_async_cfg::maybe(
186        sync(not(feature = "async"), keep_self),
187        async(feature = "async", keep_self)
188    )]
189    pub async fn measure_raw_signals(&mut self) -> Result<RawSignals, Error<I2C::Error>> {
190        self.i2c
191            .write(self.address, MEASURE_RAW_SIGNALS_COMMAND)
192            .await?;
193        self.delay.delay_ms(25).await;
194        let mut data = [0u8; 6];
195        self.i2c.read(self.address, &mut data).await?;
196        let h2: &[u8; 2] = &data[0..2].try_into().unwrap();
197        let h2_crc = data[2];
198        let ethanol: &[u8; 2] = &data[3..5].try_into().unwrap();
199        let ethanol_crc = data[5];
200        Self::check_crc(h2, h2_crc)?;
201        Self::check_crc(ethanol, ethanol_crc)?;
202        Ok(RawSignals {
203            h2: Self::get_u16_value(h2),
204            ethanol: Self::get_u16_value(ethanol),
205        })
206    }
207
208    /// Perform a soft reset.
209    #[maybe_async_cfg::maybe(
210        sync(not(feature = "async"), keep_self),
211        async(feature = "async", keep_self)
212    )]
213    pub async fn reset(&mut self) -> Result<(), Error<I2C::Error>> {
214        self.i2c.write(self.address, RESET_COMMAND).await?;
215        self.delay.delay_us(600).await; // Wait for the sensor to enter idle state
216        Ok(())
217    }
218
219    /// Set the baseline values for the air quality signals.
220    ///
221    /// The goal of this feature is to feed the baseline correction algorithm
222    /// with values that have been stored on an external memory using the
223    /// [`Sgp30::get_baseline()`] function.
224    ///
225    /// This needs to be called just after calling
226    /// [`Sgp30::initialize_air_quality_measure()`], and prior to any call to
227    /// [`Sgp30::measure_air_quality()`].
228    #[maybe_async_cfg::maybe(
229        sync(not(feature = "async"), keep_self),
230        async(feature = "async", keep_self)
231    )]
232    pub async fn set_baseline(&mut self, baseline: AirQuality) -> Result<(), Error<I2C::Error>> {
233        let mut data = [0u8; 8];
234        data[0..2].clone_from_slice(SET_BASELINE_COMMAND);
235        let tvoc = Self::get_u8_array_value(baseline.tvoc);
236        data[2..4].clone_from_slice(&tvoc);
237        data[4] = Self::calc_crc(&tvoc);
238        let co2 = Self::get_u8_array_value(baseline.co2);
239        data[5..7].clone_from_slice(&co2);
240        data[7] = Self::calc_crc(&co2);
241        self.i2c.write(self.address, &data).await?;
242        self.delay.delay_ms(10).await;
243        Ok(())
244    }
245
246    /// Feed the on-chip humidity compensation algorithm with the current
247    /// humidity to get more accurate air quality measurements.
248    ///
249    /// The given humidity is the absolute humidity in g/m³, that needs to be
250    /// measured with an external sensor such as the SHT3x.
251    ///
252    /// <div class="warning">This feature may not be available depending on
253    /// the version of your sensor.</div>
254    #[maybe_async_cfg::maybe(
255        sync(not(feature = "async"), keep_self),
256        async(feature = "async", keep_self)
257    )]
258    pub async fn set_humidity(&mut self, humidity: f32) -> Result<(), Error<I2C::Error>> {
259        if self.product_version < 0x20 {
260            return Err(Error::FeatureNotSupported);
261        }
262        let mut data = [0u8; 5];
263        data[0..2].clone_from_slice(SET_HUMIDITY_COMMAND);
264        let humidity = [
265            humidity.trunc() as u8,
266            (humidity.fract() * 256.0).trunc() as u8,
267        ];
268        data[2..4].clone_from_slice(&humidity);
269        data[4] = Self::calc_crc(&humidity);
270        self.i2c.write(self.address, &data).await?;
271        self.delay.delay_ms(10).await;
272        Ok(())
273    }
274
275    #[maybe_async_cfg::maybe(
276        sync(not(feature = "async"), keep_self),
277        async(feature = "async", keep_self)
278    )]
279    async fn get_air_quality(
280        &mut self,
281        command: &[u8],
282        wait: u32,
283    ) -> Result<AirQuality, Error<I2C::Error>> {
284        self.i2c.write(self.address, command).await?;
285        self.delay.delay_ms(wait).await;
286        let mut data = [0u8; 6];
287        self.i2c.read(self.address, &mut data).await?;
288        let co2: &[u8; 2] = &data[0..2].try_into().unwrap();
289        let co2_crc = data[2];
290        let tvoc = &data[3..5].try_into().unwrap();
291        let tvoc_crc = data[5];
292        Self::check_crc(co2, co2_crc)?;
293        Self::check_crc(tvoc, tvoc_crc)?;
294        Ok(AirQuality {
295            co2: Self::get_u16_value(co2),
296            tvoc: Self::get_u16_value(tvoc),
297        })
298    }
299
300    #[maybe_async_cfg::maybe(
301        sync(not(feature = "async"), keep_self),
302        async(feature = "async", keep_self)
303    )]
304    async fn get_feature_set_version(&mut self) -> Result<u16, Error<I2C::Error>> {
305        let mut data = [0u8; 3];
306        let mut operations = [
307            Operation::Write(GET_FEATURE_SET_VERSION_COMMAND),
308            Operation::Read(&mut data),
309        ];
310        self.i2c.transaction(self.address, &mut operations).await?;
311        let feature_set_version: &[u8; 2] = &data[0..2].try_into().unwrap();
312        let feature_set_version_crc = data[2];
313        Self::check_crc(feature_set_version, feature_set_version_crc)?;
314        Ok(Self::get_u16_value(feature_set_version))
315    }
316
317    #[maybe_async_cfg::maybe(
318        sync(not(feature = "async"), keep_self),
319        async(feature = "async", keep_self)
320    )]
321    async fn get_serial_id(&mut self) -> Result<u64, Error<I2C::Error>> {
322        self.i2c.write(self.address, GET_SERIAL_ID_COMMAND).await?;
323        self.delay.delay_us(500).await;
324        let mut data = [0u8; 9];
325        self.i2c.read(self.address, &mut data).await?;
326        let id1: &[u8; 2] = &data[0..2].try_into().unwrap();
327        let id1_crc = data[2];
328        let id2: &[u8; 2] = &data[3..5].try_into().unwrap();
329        let id2_crc = data[5];
330        let id3: &[u8; 2] = &data[6..8].try_into().unwrap();
331        let id3_crc = data[8];
332        Self::check_crc(id1, id1_crc)?;
333        Self::check_crc(id2, id2_crc)?;
334        Self::check_crc(id3, id3_crc)?;
335        Ok((Self::get_u16_value(id1) as u64) << 32
336            | (Self::get_u16_value(id2) as u64) << 16
337            | (Self::get_u16_value(id3) as u64))
338    }
339
340    fn calc_crc(data: &[u8; 2]) -> u8 {
341        let crc = Crc::<u8>::new(&CRC_8_NRSC_5);
342        let mut digest = crc.digest();
343        digest.update(data);
344        digest.finalize()
345    }
346
347    fn check_crc(data: &[u8; 2], expected_crc: u8) -> Result<(), Error<I2C::Error>> {
348        if Self::calc_crc(data) != expected_crc {
349            Err(Error::BadCrc)
350        } else {
351            Ok(())
352        }
353    }
354
355    #[inline]
356    fn get_u8_array_value(data: u16) -> [u8; 2] {
357        [(data >> 8) as u8, (data & 0xff) as u8]
358    }
359
360    #[inline]
361    fn get_u16_value(data: &[u8; 2]) -> u16 {
362        (data[0] as u16) << 8 | (data[1] as u16)
363    }
364}
365
366#[cfg(test)]
367mod tests {
368    use embedded_hal::i2c::ErrorKind;
369    use embedded_hal_mock::eh1::delay::StdSleep as Delay;
370    use embedded_hal_mock::eh1::i2c::{Mock as I2cMock, Transaction as I2cTransaction};
371
372    use super::*;
373
374    fn create_device() -> Sgp30<I2cMock, Delay> {
375        let expectations = [
376            I2cTransaction::write(DEFAULT_I2C_ADDRESS, GET_SERIAL_ID_COMMAND.to_vec()),
377            I2cTransaction::read(
378                DEFAULT_I2C_ADDRESS,
379                [0x01, 0x02, 0x17, 0x03, 0x04, 0x68, 0x05, 0x06, 0x50].to_vec(),
380            ),
381            I2cTransaction::transaction_start(DEFAULT_I2C_ADDRESS),
382            I2cTransaction::write(
383                DEFAULT_I2C_ADDRESS,
384                GET_FEATURE_SET_VERSION_COMMAND.to_vec(),
385            ),
386            I2cTransaction::read(DEFAULT_I2C_ADDRESS, [0x00, 0x20, 0x07].to_vec()),
387            I2cTransaction::transaction_end(DEFAULT_I2C_ADDRESS),
388        ];
389        let i2c = I2cMock::new(&expectations);
390        let mut device = Sgp30::new(i2c, DEFAULT_I2C_ADDRESS, Delay {}).unwrap();
391        device.i2c.done();
392        device
393    }
394
395    #[test]
396    fn chip_not_detected() {
397        let expectations =
398            [
399                I2cTransaction::write(DEFAULT_I2C_ADDRESS, GET_SERIAL_ID_COMMAND.to_vec())
400                    .with_error(ErrorKind::Other),
401            ];
402        let mut i2c = I2cMock::new(&expectations);
403        assert!(matches!(
404            Sgp30::new(i2c.by_ref(), DEFAULT_I2C_ADDRESS, Delay {}),
405            Err(Error::ChipNotDetected)
406        ));
407        i2c.done();
408    }
409
410    #[test]
411    fn invalid_product() {
412        let expectations = [
413            I2cTransaction::write(DEFAULT_I2C_ADDRESS, GET_SERIAL_ID_COMMAND.to_vec()),
414            I2cTransaction::read(
415                DEFAULT_I2C_ADDRESS,
416                [0x01, 0x02, 0x17, 0x03, 0x04, 0x68, 0x05, 0x06, 0x50].to_vec(),
417            ),
418            I2cTransaction::transaction_start(DEFAULT_I2C_ADDRESS),
419            I2cTransaction::write(
420                DEFAULT_I2C_ADDRESS,
421                GET_FEATURE_SET_VERSION_COMMAND.to_vec(),
422            ),
423            I2cTransaction::read(DEFAULT_I2C_ADDRESS, [0x00, 0x00, 0x81].to_vec()),
424            I2cTransaction::transaction_end(DEFAULT_I2C_ADDRESS),
425        ];
426        let mut i2c = I2cMock::new(&expectations);
427        assert!(matches!(
428            Sgp30::new(i2c.by_ref(), DEFAULT_I2C_ADDRESS, Delay {}),
429            Err(Error::InvalidProduct)
430        ));
431        i2c.done();
432    }
433
434    #[test]
435    fn bad_crc() {
436        let expectations = [
437            I2cTransaction::write(DEFAULT_I2C_ADDRESS, GET_SERIAL_ID_COMMAND.to_vec()),
438            I2cTransaction::read(
439                DEFAULT_I2C_ADDRESS,
440                [0x01, 0x02, 0x17, 0x03, 0x04, 0x68, 0x05, 0x06, 0x50].to_vec(),
441            ),
442            I2cTransaction::transaction_start(DEFAULT_I2C_ADDRESS),
443            I2cTransaction::write(
444                DEFAULT_I2C_ADDRESS,
445                GET_FEATURE_SET_VERSION_COMMAND.to_vec(),
446            ),
447            I2cTransaction::read(DEFAULT_I2C_ADDRESS, [0x00, 0x00, 0x07].to_vec()),
448            I2cTransaction::transaction_end(DEFAULT_I2C_ADDRESS),
449        ];
450        let mut i2c = I2cMock::new(&expectations);
451        assert!(matches!(
452            Sgp30::new(i2c.by_ref(), DEFAULT_I2C_ADDRESS, Delay {}),
453            Err(Error::BadCrc)
454        ));
455        i2c.done();
456    }
457
458    #[test]
459    fn get_baseline() {
460        let expectations = [
461            I2cTransaction::write(DEFAULT_I2C_ADDRESS, GET_BASELINE_COMMAND.to_vec()),
462            I2cTransaction::read(
463                DEFAULT_I2C_ADDRESS,
464                [0x02, 0x76, 0x06, 0x02, 0xdd, 0x10].to_vec(),
465            ),
466        ];
467        let mut device = create_device();
468        device.i2c.update_expectations(&expectations);
469        device.get_baseline().unwrap();
470        device.i2c.done();
471    }
472
473    #[test]
474    fn initialize_air_quality_measure() {
475        let expectations = [I2cTransaction::write(
476            DEFAULT_I2C_ADDRESS,
477            INIT_AIR_QUALITY_COMMAND.to_vec(),
478        )];
479        let mut device = create_device();
480        device.i2c.update_expectations(&expectations);
481        device.initialize_air_quality_measure().unwrap();
482        device.i2c.done();
483    }
484
485    #[test]
486    fn measure_air_quality() {
487        let expectations = [
488            I2cTransaction::write(DEFAULT_I2C_ADDRESS, MEASURE_AIR_QUALITY_COMMAND.to_vec()),
489            I2cTransaction::read(
490                DEFAULT_I2C_ADDRESS,
491                [0x02, 0x76, 0x06, 0x02, 0xdd, 0x10].to_vec(),
492            ),
493        ];
494        let mut device = create_device();
495        device.i2c.update_expectations(&expectations);
496        device.measure_air_quality().unwrap();
497        device.i2c.done();
498    }
499
500    #[test]
501    fn measure_raw_signals() {
502        let expectations = [
503            I2cTransaction::write(DEFAULT_I2C_ADDRESS, MEASURE_RAW_SIGNALS_COMMAND.to_vec()),
504            I2cTransaction::read(
505                DEFAULT_I2C_ADDRESS,
506                [0x00, 0x24, 0xc3, 0x01, 0x51, 0x3a].to_vec(),
507            ),
508        ];
509        let mut device = create_device();
510        device.i2c.update_expectations(&expectations);
511        device.measure_raw_signals().unwrap();
512        device.i2c.done();
513    }
514
515    #[test]
516    fn reset() {
517        let expectations = [I2cTransaction::write(
518            DEFAULT_I2C_ADDRESS,
519            RESET_COMMAND.to_vec(),
520        )];
521        let mut device = create_device();
522        device.i2c.update_expectations(&expectations);
523        device.reset().unwrap();
524        device.i2c.done();
525    }
526
527    #[test]
528    fn set_baseline() {
529        let air_quality = AirQuality {
530            co2: 630,
531            tvoc: 733,
532        };
533        let expectations = [I2cTransaction::write(
534            DEFAULT_I2C_ADDRESS,
535            [
536                SET_BASELINE_COMMAND[0],
537                SET_BASELINE_COMMAND[1],
538                0x02,
539                0xdd,
540                0x10,
541                0x02,
542                0x76,
543                0x06,
544            ]
545            .to_vec(),
546        )];
547        let mut device = create_device();
548        device.i2c.update_expectations(&expectations);
549        device.set_baseline(air_quality).unwrap();
550        device.i2c.done();
551    }
552
553    #[test]
554    fn set_humidity() {
555        let expectations = [I2cTransaction::write(
556            DEFAULT_I2C_ADDRESS,
557            [
558                SET_HUMIDITY_COMMAND[0],
559                SET_HUMIDITY_COMMAND[1],
560                0x09,
561                0x35,
562                0x72,
563            ]
564            .to_vec(),
565        )];
566        let mut device = create_device();
567        device.i2c.update_expectations(&expectations);
568        device.set_humidity(9.21).unwrap();
569        device.i2c.done();
570    }
571
572    #[test]
573    fn set_humidity_feature_not_supported() {
574        let expectations = [
575            I2cTransaction::write(DEFAULT_I2C_ADDRESS, GET_SERIAL_ID_COMMAND.to_vec()),
576            I2cTransaction::read(
577                DEFAULT_I2C_ADDRESS,
578                [0x01, 0x02, 0x17, 0x03, 0x04, 0x68, 0x05, 0x06, 0x50].to_vec(),
579            ),
580            I2cTransaction::transaction_start(DEFAULT_I2C_ADDRESS),
581            I2cTransaction::write(
582                DEFAULT_I2C_ADDRESS,
583                GET_FEATURE_SET_VERSION_COMMAND.to_vec(),
584            ),
585            I2cTransaction::read(DEFAULT_I2C_ADDRESS, [0x00, 0x1A, 0x19].to_vec()),
586            I2cTransaction::transaction_end(DEFAULT_I2C_ADDRESS),
587        ];
588        let i2c = I2cMock::new(&expectations);
589        let mut device = Sgp30::new(i2c, DEFAULT_I2C_ADDRESS, Delay {}).unwrap();
590        assert!(matches!(
591            device.set_humidity(9.21),
592            Err(Error::FeatureNotSupported)
593        ));
594        device.i2c.done();
595    }
596}