use crate::commands::Command;
use crate::error::{Error, SelfTestError};
use crate::types::RawSensorData;
use embedded_hal as hal;
use hal::blocking::delay::DelayMs;
use hal::blocking::i2c::{Read, Write, WriteRead};
use sensirion_i2c::{crc8, i2c};
const SGP41_I2C_ADDRESS: u8 = 0x59;
#[derive(Debug, Default)]
pub struct Sgp41<I2C, D> {
i2c: I2C,
delay: D,
temperature_offset: i16,
}
impl<I2C, D, E> Sgp41<I2C, D>
where
I2C: Read<Error = E> + Write<Error = E> + WriteRead<Error = E>,
D: DelayMs<u32>,
{
pub fn new(i2c: I2C, delay: D) -> Self {
Sgp41 {
i2c,
delay,
temperature_offset: 0,
}
}
pub fn execute_conditioning(&mut self) -> Result<u16, Error<E>> {
let mut buf = [0; 3];
self.read_cmd_args(Command::ExecuteConditioning, &[0x8000, 0x6666], &mut buf)?;
Ok(u16::from_be_bytes([buf[0], buf[1]]))
}
pub fn measure_raw(&mut self) -> Result<RawSensorData, Error<E>> {
let mut buf = [0; 6];
self.read_cmd_args(Command::MeasureRawSignals, &[0x8000, 0x6666], &mut buf)?;
let voc_ticks = u16::from_be_bytes([buf[0], buf[1]]);
let nox_ticks = u16::from_be_bytes([buf[3], buf[4]]);
let data = RawSensorData {
voc_ticks,
nox_ticks,
};
Ok(data)
}
pub fn measure_raw_compensated(
&mut self,
humidity: u8,
temperature: i16,
) -> Result<RawSensorData, Error<E>> {
assert!(humidity <= 100 && temperature >= -45 && temperature <= 130);
let humidity_ticks = humidity as u16 * u16::MAX / 100;
let temperature_ticks =
(temperature + 45 + self.temperature_offset) as u16 * u16::MAX / 175;
let mut buf = [0; 6];
self.read_cmd_args(
Command::MeasureRawSignals,
&[humidity_ticks, temperature_ticks],
&mut buf,
)?;
let voc_ticks = u16::from_be_bytes([buf[0], buf[1]]);
let nox_ticks = u16::from_be_bytes([buf[3], buf[4]]);
let data = RawSensorData {
voc_ticks,
nox_ticks,
};
Ok(data)
}
pub fn execute_self_test(&mut self) -> Result<(), Error<E>> {
let mut buf = [0; 3];
self.read_cmd(Command::ExecuteConditioning, &mut buf)?;
let err = u16::from_be_bytes([buf[0], buf[1]]) & 0b11;
match err {
0b00 => Ok(()),
0b01 => Err(Error::SelfTest(SelfTestError::Voc)),
0b10 => Err(Error::SelfTest(SelfTestError::Nox)),
0b11 => Err(Error::SelfTest(SelfTestError::All)),
_ => Err(Error::SelfTest(SelfTestError::Undefined)),
}
}
pub fn turn_heater_off(&mut self) -> Result<(), Error<E>> {
self.write_cmd(Command::TurnHeaterOff)
}
pub fn get_serial_number(&mut self) -> Result<u64, Error<E>> {
let mut buf = [0; 9];
self.read_cmd(Command::GetSerialNumber, &mut buf)?;
let serial = u64::from(buf[0]) << 40
| u64::from(buf[1]) << 32
| u64::from(buf[3]) << 24
| u64::from(buf[4]) << 16
| u64::from(buf[6]) << 8
| u64::from(buf[7]);
Ok(serial)
}
pub fn soft_reset(&mut self) -> Result<(), Error<E>> {
self.write_cmd(Command::SoftReset)
}
pub fn set_temperature_offset(&mut self, offset: i8) {
self.temperature_offset += offset as i16;
}
fn write_cmd(&mut self, cmd: Command) -> Result<(), Error<E>> {
let (command, delay) = cmd.as_tuple();
i2c::write_command(&mut self.i2c, SGP41_I2C_ADDRESS, command).map_err(Error::I2c)?;
self.delay.delay_ms(delay);
Ok(())
}
fn write_cmd_args(&mut self, cmd: Command, args: &[u16]) -> Result<(), Error<E>> {
let (command, delay) = cmd.as_tuple();
let mut buf = [0; 8];
assert!(command.to_ne_bytes().len() + args.len() * 3 < buf.len());
buf[0..2].copy_from_slice(&command.to_be_bytes());
let mut i = 2;
for arg in args {
let end = i + 2;
let be_arg = arg.to_be_bytes();
buf[i..end].copy_from_slice(&be_arg);
buf[end] = crc8::calculate(&be_arg);
i += 3;
}
self.i2c
.write(SGP41_I2C_ADDRESS, &buf[0..i])
.map_err(Error::I2c)?;
self.delay.delay_ms(delay);
Ok(())
}
fn read_cmd(&mut self, cmd: Command, data: &mut [u8]) -> Result<(), Error<E>> {
self.write_cmd(cmd)?;
i2c::read_words_with_crc(&mut self.i2c, SGP41_I2C_ADDRESS, data)?;
Ok(())
}
fn read_cmd_args(
&mut self,
cmd: Command,
args: &[u16],
data: &mut [u8],
) -> Result<(), Error<E>> {
self.write_cmd_args(cmd, args)?;
i2c::read_words_with_crc(&mut self.i2c, SGP41_I2C_ADDRESS, data)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use embedded_hal_mock as hal;
use self::hal::delay::MockNoop as DelayMock;
use self::hal::i2c::{Mock as I2cMock, Transaction};
use super::*;
#[test]
fn test_get_serial_number() {
let (cmd, _) = Command::GetSerialNumber.as_tuple();
let expectations = [
Transaction::write(SGP41_I2C_ADDRESS, cmd.to_be_bytes().to_vec()),
Transaction::read(
SGP41_I2C_ADDRESS,
vec![0xbe, 0xef, 0x92, 0xbe, 0xef, 0x92, 0xbe, 0xef, 0x92],
),
];
let mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock, DelayMock);
let serial = sensor.get_serial_number().unwrap();
assert_eq!(serial, 0xbeefbeefbeef);
}
}