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