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
16pub 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#[derive(Debug)]
31pub enum Error<I2cE>
32where
33 I2cE: hal::i2c::Error,
34{
35 I2c(I2cE),
37 ChipNotDetected,
39 InvalidProduct,
42 FeatureNotSupported,
45 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#[derive(Clone, Copy, Debug, Default)]
60pub struct AirQuality {
61 pub co2: u16,
64 pub tvoc: u16,
66}
67
68#[derive(Clone, Copy, Debug, Default)]
70pub struct RawSignals {
71 pub ethanol: u16,
73 pub h2: u16,
75}
76
77#[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 #[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 if device.get_serial_id().await.is_err() {
110 return Err(Error::ChipNotDetected);
111 }
112
113 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 #[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 #[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 #[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 #[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 #[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; Ok(())
217 }
218
219 #[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 #[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}