#![deny(unsafe_code)]
#![deny(missing_docs)]
#![cfg_attr(not(test), no_std)]
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
use byteorder::{BigEndian, ByteOrder};
use embedded_hal as hal;
use sensirion_i2c::{crc8, i2c};
use crate::hal::{
delay::DelayNs,
i2c::{ErrorType, I2c},
};
#[cfg(feature = "embedded-hal-async")]
mod async_impl;
#[cfg(feature = "embedded-hal-async")]
pub use async_impl::Sgp30Async;
mod types;
pub use crate::types::{Baseline, FeatureSet, Humidity, Measurement, ProductType, RawSignals};
#[derive(Debug)]
pub enum Error<E> {
I2cWrite(E),
I2cRead(E),
Crc,
NotInitialized,
}
impl<I> From<i2c::Error<I>> for Error<I::Error>
where
I: ErrorType,
{
fn from(err: i2c::Error<I>) -> Self {
match err {
i2c::Error::Crc => Error::Crc,
i2c::Error::I2cWrite(e) => Error::I2cRead(e),
i2c::Error::I2cRead(e) => Error::I2cWrite(e),
}
}
}
#[derive(Debug, Copy, Clone)]
enum Command {
GetSerial,
SelfTest,
InitAirQuality,
MeasureAirQuality,
MeasureRawSignals,
GetBaseline,
SetBaseline,
SetHumidity,
GetFeatureSet,
}
impl Command {
fn as_bytes(self) -> [u8; 2] {
match self {
Command::GetSerial => [0x36, 0x82],
Command::SelfTest => [0x20, 0x32],
Command::InitAirQuality => [0x20, 0x03],
Command::MeasureAirQuality => [0x20, 0x08],
Command::MeasureRawSignals => [0x20, 0x50],
Command::GetBaseline => [0x20, 0x15],
Command::SetBaseline => [0x20, 0x1E],
Command::SetHumidity => [0x20, 0x61],
Command::GetFeatureSet => [0x20, 0x2F],
}
}
fn as_bytes_with_data<'buf>(self, buf: &'buf mut [u8; 8], data: &[u8]) -> &'buf [u8] {
assert!(data.len() == 2 || data.len() == 4);
buf[0..2].copy_from_slice(&self.as_bytes());
buf[2..4].copy_from_slice(&data[0..2]);
buf[4] = crc8::calculate(&data[0..2]);
if data.len() > 2 {
buf[5..7].copy_from_slice(&data[2..4]);
buf[7] = crc8::calculate(&data[2..4]);
}
if data.len() > 2 {
&buf[0..8]
} else {
&buf[0..5]
}
}
}
#[derive(Debug, Default)]
pub struct Sgp30<I2C, D> {
i2c: I2C,
address: u8,
delay: D,
initialized: bool,
}
const SELFTEST_SUCCESS: &[u8] = &[0xd4, 0x00];
impl<I2C, D> Sgp30<I2C, D>
where
I2C: I2c,
D: DelayNs,
{
pub fn new(i2c: I2C, address: u8, delay: D) -> Self {
Sgp30 {
i2c,
address,
delay,
initialized: false,
}
}
pub fn destroy(self) -> I2C {
self.i2c
}
fn send_command(&mut self, command: Command) -> Result<(), Error<I2C::Error>> {
self.i2c
.write(self.address, &command.as_bytes())
.map_err(Error::I2cWrite)
}
fn send_command_and_data(
&mut self,
command: Command,
data: &[u8],
) -> Result<(), Error<I2C::Error>> {
let mut buf = [0; 2 + 6 ];
let payload = command.as_bytes_with_data(&mut buf, data);
self.i2c
.write(self.address, payload)
.map_err(Error::I2cWrite)
}
pub fn serial(&mut self) -> Result<[u8; 6], Error<I2C::Error>> {
self.send_command(Command::GetSerial)?;
self.delay.delay_us(500);
let mut buf = [0; 9];
i2c::read_words_with_crc(&mut self.i2c, self.address, &mut buf)?;
Ok([buf[0], buf[1], buf[3], buf[4], buf[6], buf[7]])
}
pub fn selftest(&mut self) -> Result<bool, Error<I2C::Error>> {
self.send_command(Command::SelfTest)?;
self.delay.delay_ms(220);
let mut buf = [0; 3];
i2c::read_words_with_crc(&mut self.i2c, self.address, &mut buf)?;
Ok(&buf[0..2] == SELFTEST_SUCCESS)
}
pub fn init(&mut self) -> Result<(), Error<I2C::Error>> {
if self.initialized {
return Ok(());
}
self.force_init()
}
pub fn force_init(&mut self) -> Result<(), Error<I2C::Error>> {
self.send_command(Command::InitAirQuality)?;
self.delay.delay_ms(10);
self.initialized = true;
Ok(())
}
pub fn measure(&mut self) -> Result<Measurement, Error<I2C::Error>> {
if !self.initialized {
return Err(Error::NotInitialized);
}
self.send_command(Command::MeasureAirQuality)?;
self.delay.delay_ms(12);
let mut buf = [0; 6];
i2c::read_words_with_crc(&mut self.i2c, self.address, &mut buf)?;
Ok(Measurement::from_bytes(&buf))
}
pub fn measure_raw_signals(&mut self) -> Result<RawSignals, Error<I2C::Error>> {
if !self.initialized {
return Err(Error::NotInitialized);
}
self.send_command(Command::MeasureRawSignals)?;
self.delay.delay_ms(25);
let mut buf = [0; 6];
i2c::read_words_with_crc(&mut self.i2c, self.address, &mut buf)?;
Ok(RawSignals::from_bytes(&buf))
}
pub fn get_baseline(&mut self) -> Result<Baseline, Error<I2C::Error>> {
self.send_command(Command::GetBaseline)?;
self.delay.delay_ms(10);
let mut buf = [0; 6];
i2c::read_words_with_crc(&mut self.i2c, self.address, &mut buf)?;
Ok(Baseline::from_bytes(&buf))
}
pub fn set_baseline(&mut self, baseline: &Baseline) -> Result<(), Error<I2C::Error>> {
if !self.initialized {
return Err(Error::NotInitialized);
}
let mut buf = [0; 4];
BigEndian::write_u16(&mut buf[0..2], baseline.tvoc);
BigEndian::write_u16(&mut buf[2..4], baseline.co2eq);
self.send_command_and_data(Command::SetBaseline, &buf)?;
self.delay.delay_ms(10);
Ok(())
}
pub fn set_humidity(&mut self, humidity: Option<&Humidity>) -> Result<(), Error<I2C::Error>> {
if !self.initialized {
return Err(Error::NotInitialized);
}
let buf = match humidity {
Some(humi) => humi.as_bytes(),
None => [0, 0],
};
self.send_command_and_data(Command::SetHumidity, &buf)?;
self.delay.delay_ms(10);
Ok(())
}
pub fn get_feature_set(&mut self) -> Result<FeatureSet, Error<I2C::Error>> {
self.send_command(Command::GetFeatureSet)?;
self.delay.delay_ms(2);
let mut buf = [0; 3];
i2c::read_words_with_crc(&mut self.i2c, self.address, &mut buf)?;
Ok(FeatureSet::parse(buf[0], buf[1]))
}
}
#[cfg(test)]
mod tests {
use embedded_hal_mock as hal;
use self::hal::eh1::{
delay::NoopDelay,
i2c::{Mock as I2cMock, Transaction},
};
use super::*;
#[test]
fn serial() {
let expectations = [
Transaction::write(0x58, Command::GetSerial.as_bytes()[..].into()),
Transaction::read(0x58, vec![0, 0, 129, 0, 100, 254, 204, 130, 135]),
];
let mock = I2cMock::new(&expectations);
let mut sgp = Sgp30::new(mock, 0x58, NoopDelay);
let serial = sgp.serial().unwrap();
assert_eq!(serial, [0, 0, 0, 100, 204, 130]);
sgp.destroy().done();
}
#[test]
fn selftest_ok() {
let expectations = [
Transaction::write(0x58, Command::SelfTest.as_bytes()[..].into()),
Transaction::read(0x58, vec![0xD4, 0x00, 0xC6]),
];
let mock = I2cMock::new(&expectations);
let mut sgp = Sgp30::new(mock, 0x58, NoopDelay);
assert!(sgp.selftest().unwrap());
sgp.destroy().done();
}
#[test]
fn selftest_fail() {
let expectations = [
Transaction::write(0x58, Command::SelfTest.as_bytes()[..].into()),
Transaction::read(0x58, vec![0x12, 0x34, 0x37]),
];
let mock = I2cMock::new(&expectations);
let mut sgp = Sgp30::new(mock, 0x58, NoopDelay);
assert!(!sgp.selftest().unwrap());
sgp.destroy().done();
}
#[test]
fn measure_initialization_required() {
let mock = I2cMock::new(&[]);
let mut sgp = Sgp30::new(mock, 0x58, NoopDelay);
match sgp.measure() {
Err(Error::NotInitialized) => {}
Ok(_) => panic!("Error::NotInitialized not returned"),
Err(_) => panic!("Wrong error returned"),
}
sgp.destroy().done();
}
#[test]
fn measure_success() {
let expectations = [
Transaction::write(0x58, Command::InitAirQuality.as_bytes()[..].into()),
Transaction::write(0x58, Command::MeasureAirQuality.as_bytes()[..].into()),
Transaction::read(0x58, vec![0x12, 0x34, 0x37, 0xD4, 0x02, 0xA4]),
];
let mock = I2cMock::new(&expectations);
let mut sgp = Sgp30::new(mock, 0x58, NoopDelay);
sgp.init().unwrap();
let measurements = sgp.measure().unwrap();
assert_eq!(measurements.co2eq_ppm, 4_660);
assert_eq!(measurements.tvoc_ppb, 54_274);
sgp.destroy().done();
}
#[test]
fn get_baseline() {
let expectations = [
Transaction::write(0x58, Command::InitAirQuality.as_bytes()[..].into()),
Transaction::write(0x58, Command::GetBaseline.as_bytes()[..].into()),
Transaction::read(0x58, vec![0x12, 0x34, 0x37, 0xD4, 0x02, 0xA4]),
];
let mock = I2cMock::new(&expectations);
let mut sgp = Sgp30::new(mock, 0x58, NoopDelay);
sgp.init().unwrap();
let baseline = sgp.get_baseline().unwrap();
assert_eq!(baseline.co2eq, 4_660);
assert_eq!(baseline.tvoc, 54_274);
sgp.destroy().done();
}
#[test]
fn set_baseline() {
#[rustfmt::skip]
let expectations = [
Transaction::write(0x58, Command::InitAirQuality.as_bytes()[..].into()),
Transaction::write(0x58, vec![
0x20, 0x1E,
0x56, 0x78, 0x7D, 0x12, 0x34, 0x37,
]),
];
let mock = I2cMock::new(&expectations);
let mut sgp = Sgp30::new(mock, 0x58, NoopDelay);
sgp.init().unwrap();
let baseline = Baseline {
co2eq: 0x1234,
tvoc: 0x5678,
};
sgp.set_baseline(&baseline).unwrap();
sgp.destroy().done();
}
#[test]
fn set_humidity() {
#[rustfmt::skip]
let expectations = [
Transaction::write(0x58, Command::InitAirQuality.as_bytes()[..].into()),
Transaction::write(0x58, vec![
0x20, 0x61,
0x0F, 0x80, 0x62,
]),
];
let mock = I2cMock::new(&expectations);
let mut sgp = Sgp30::new(mock, 0x58, NoopDelay);
sgp.init().unwrap();
let humidity = Humidity::from_f32(15.5).unwrap();
sgp.set_humidity(Some(&humidity)).unwrap();
sgp.destroy().done();
}
#[test]
fn set_humidity_none() {
#[rustfmt::skip]
let expectations = [
Transaction::write(0x58, Command::InitAirQuality.as_bytes()[..].into()),
Transaction::write(0x58, vec![
0x20, 0x61,
0x00, 0x00, 0x81,
]),
];
let mock = I2cMock::new(&expectations);
let mut sgp = Sgp30::new(mock, 0x58, NoopDelay);
sgp.init().unwrap();
sgp.set_humidity(None).unwrap();
sgp.destroy().done();
}
#[test]
fn get_feature_set() {
let expectations = [
Transaction::write(0x58, Command::InitAirQuality.as_bytes()[..].into()),
Transaction::write(0x58, Command::GetFeatureSet.as_bytes()[..].into()),
Transaction::read(0x58, vec![0x00, 0x42, 0xDE]),
];
let mock = I2cMock::new(&expectations);
let mut sgp = Sgp30::new(mock, 0x58, NoopDelay);
sgp.init().unwrap();
let feature_set = sgp.get_feature_set().unwrap();
assert_eq!(feature_set.product_type, ProductType::Sgp30);
assert_eq!(feature_set.product_version, 0x42);
sgp.destroy().done();
}
#[test]
fn measure_raw_signals() {
let expectations = [
Transaction::write(0x58, Command::InitAirQuality.as_bytes()[..].into()),
Transaction::write(0x58, Command::MeasureRawSignals.as_bytes()[..].into()),
Transaction::read(0x58, vec![0x12, 0x34, 0x37, 0x56, 0x78, 0x7D]),
];
let mock = I2cMock::new(&expectations);
let mut sgp = Sgp30::new(mock, 0x58, NoopDelay);
sgp.init().unwrap();
let signals = sgp.measure_raw_signals().unwrap();
assert_eq!(signals.h2, (0x12 << 8) + 0x34);
assert_eq!(signals.ethanol, (0x56 << 8) + 0x78);
sgp.destroy().done();
}
}