#![cfg_attr(not(test), no_std)]
#![allow(non_snake_case)]
#![allow(dead_code)]
use embedded_hal as hal;
use hal::delay::DelayNs;
use hal::i2c::I2c;
use sensirion_i2c::{crc8, i2c};
use gas_index_algorithm::{GasIndexAlgorithm, AlgorithmType};
#[derive(Debug)]
pub enum Error<E> {
I2c(E),
Crc,
SelfTest,
Conditioning,
}
impl<E, I> From<i2c::Error<I>> for Error<E>
where
I: I2c<Error = E>,
{
fn from(err: i2c::Error<I>) -> Self {
match err {
i2c::Error::Crc => Error::Crc,
i2c::Error::I2cWrite(e) => Error::I2c(e),
i2c::Error::I2cRead(e) => Error::I2c(e),
}
}
}
#[derive(Debug, Copy, Clone)]
enum Command {
MeasurementRaw,
Conditioning,
Serial,
HeaterOff,
MeasureTest,
SoftReset,
}
impl Command {
fn as_tuple(self) -> (u16, u32) {
match self {
Command::MeasurementRaw => (0x2619, 50), Command::Conditioning => (0x2612, 50), Command::Serial => (0x3682, 1), Command::HeaterOff => (0x3615, 1), Command::MeasureTest => (0x280e, 320), Command::SoftReset => (0x0006, 1), }
}
}
pub struct Sgp41<I2C, D> {
i2c: I2C,
address: u8,
delay: D,
temperature_offset: i16,
voc_algorithm: GasIndexAlgorithm,
nox_algorithm: GasIndexAlgorithm,
}
impl<I2C, D, E> Sgp41<I2C, D>
where
I2C: hal::i2c::I2c<Error = E>,
D: DelayNs,
{
pub fn new(i2c: I2C, address: u8, delay: D) -> Self {
Sgp41 {
i2c,
address,
delay,
temperature_offset: 0,
voc_algorithm: GasIndexAlgorithm::new(AlgorithmType::Voc, 1.0),
nox_algorithm: GasIndexAlgorithm::new(AlgorithmType::Nox, 1.0),
}
}
fn delayed_read_cmd(&mut self, cmd: Command, data: &mut [u8]) -> Result<(), Error<E>> {
self.write_command(cmd)?;
i2c::read_words_with_crc(&mut self.i2c, self.address, data)?;
Ok(())
}
fn write_command_with_args(&mut self, cmd: Command, data: &[u8]) -> Result<(), Error<E>> {
const MAX_TX_BUFFER: usize = 14;
let mut transfer_buffer = [0; MAX_TX_BUFFER];
let size = data.len();
assert!(size < 2 + size + size / 2);
let (command, delay) = cmd.as_tuple();
transfer_buffer[0..2].copy_from_slice(&command.to_be_bytes());
let mut i = 2;
for chunk in data.chunks(2) {
let end = i + 2;
transfer_buffer[i..end].copy_from_slice(chunk);
transfer_buffer[end] = crc8::calculate(chunk);
i += 3;
}
self.i2c
.write(self.address, &transfer_buffer[0..i])
.map_err(Error::I2c)?;
self.delay.delay_ms(delay);
Ok(())
}
fn write_command(&mut self, cmd: Command) -> Result<(), Error<E>> {
let (command, delay) = cmd.as_tuple();
i2c::write_command_u16(&mut self.i2c, self.address, command).map_err(Error::I2c)?;
self.delay.delay_ms(delay);
Ok(())
}
pub fn self_test(&mut self) -> Result<&mut Self, Error<E>> {
const MEASURE_TEST_OK: u16 = 0xd400;
let mut data = [0; 3];
self.delayed_read_cmd(Command::MeasureTest, &mut data)?;
let result = u16::from_be_bytes([data[0], data[1]]);
if result != MEASURE_TEST_OK {
Err(Error::SelfTest)
} else {
Ok(self)
}
}
#[inline]
pub fn turn_heater_off(&mut self) -> Result<&Self, Error<E>> {
self.write_command(Command::HeaterOff)?;
Ok(self)
}
#[inline]
pub fn reset(&mut self) -> Result<&Self, Error<E>> {
self.write_command(Command::SoftReset)?;
Ok(self)
}
#[inline]
pub fn measure_voc_index(&mut self) -> Result<u16, Error<E>> {
let raw = self.measure_raw_with_rht(50000, 25000)?;
Ok(self.voc_algorithm.process(raw as i32) as u16)
}
#[inline]
pub fn measure_voc_index_with_rht(&mut self, humidity: u16, temperature: i16) -> Result<u16, Error<E>> {
let raw = self.measure_raw_with_rht(humidity, temperature)?;
Ok(self.voc_algorithm.process(raw as i32) as u16)
}
#[inline]
pub fn measure_raw(&mut self) -> Result<u16, Error<E>> {
self.measure_raw_with_rht(50000, 25000)
}
pub fn measure_raw_with_rht(&mut self, humidity: u16, temperature: i16) -> Result<u16, Error<E>> {
let (voc_raw, _nox_raw) = self.measure_raw_signals_with_rht(humidity, temperature)?;
Ok(voc_raw)
}
pub fn measure_raw_signals_with_rht(&mut self, humidity: u16, temperature: i16) -> Result<(u16, u16), Error<E>> {
let mut data = [0; 6];
let (hum_ticks, temp_ticks) = self.convert_rht(humidity as u32, temperature as i32);
let mut params = [0u8; 4];
params[0..2].copy_from_slice(&hum_ticks.to_be_bytes());
params[2..4].copy_from_slice(&temp_ticks.to_be_bytes());
self.write_command_with_args(Command::MeasurementRaw, ¶ms)?;
i2c::read_words_with_crc(&mut self.i2c, self.address, &mut data)?;
let voc_raw = u16::from_be_bytes([data[0], data[1]]);
let nox_raw = u16::from_be_bytes([data[3], data[4]]);
Ok((voc_raw, nox_raw))
}
#[inline]
pub fn measure_raw_signals(&mut self) -> Result<(u16, u16), Error<E>> {
self.measure_raw_signals_with_rht(50000, 25000)
}
#[inline]
pub fn measure_raw_nox(&mut self) -> Result<u16, Error<E>> {
let (_voc_raw, nox_raw) = self.measure_raw_signals_with_rht(50000, 25000)?;
Ok(nox_raw)
}
#[inline]
pub fn measure_raw_nox_with_rht(&mut self, humidity: u16, temperature: i16) -> Result<u16, Error<E>> {
let (_voc_raw, nox_raw) = self.measure_raw_signals_with_rht(humidity, temperature)?;
Ok(nox_raw)
}
#[inline]
pub fn measure_nox_index(&mut self) -> Result<u16, Error<E>> {
let nox_raw = self.measure_raw_nox_with_rht(50000, 25000)?;
let nox_index = self.nox_algorithm.process(nox_raw as i32) as u16;
Ok(nox_index)
}
#[inline]
pub fn measure_nox_index_with_rht(&mut self, humidity: u16, temperature: i16) -> Result<u16, Error<E>> {
let nox_raw = self.measure_raw_nox_with_rht(humidity, temperature)?;
Ok(self.nox_algorithm.process(nox_raw as i32) as u16)
}
#[inline]
pub fn measure_indices(&mut self) -> Result<(u16, u16), Error<E>> {
self.measure_indices_with_rht(50000, 25000)
}
pub fn measure_indices_with_rht(&mut self, humidity: u16, temperature: i16) -> Result<(u16, u16), Error<E>> {
let (voc_raw, nox_raw) = self.measure_raw_signals_with_rht(humidity, temperature)?;
let voc_index = self.voc_algorithm.process(voc_raw as i32) as u16;
let nox_index = self.nox_algorithm.process(nox_raw as i32) as u16;
Ok((voc_index, nox_index))
}
fn convert_rht(&self, humidity: u32, temperature: i32) -> (u16, u16) {
let mut temperature = temperature;
let mut humidity = humidity;
if humidity > 100000 {
humidity = 100000;
}
temperature += self.temperature_offset as i32;
temperature = temperature.clamp(-45000, 129760);
let humidity_sensor_format = ((humidity * 671) >> 10) as u16;
let temperature_sensor_format = (((temperature + 45000) * 3) >> 3) as u16;
(humidity_sensor_format, temperature_sensor_format)
}
#[inline]
pub fn set_temperature_offset(&mut self, offset: i16) -> Result<&mut Self, Error<E>> {
self.temperature_offset += offset;
Ok(self)
}
pub fn get_temperature_offset(&mut self) -> Result<i16, Error<E>> {
Ok(self.temperature_offset)
}
pub fn execute_conditioning(&mut self) -> Result<u16, Error<E>> {
let mut data = [0; 3];
let (hum_ticks, temp_ticks) = self.convert_rht(50000, 25000);
let mut params = [0u8; 4];
params[0..2].copy_from_slice(&hum_ticks.to_be_bytes());
params[2..4].copy_from_slice(&temp_ticks.to_be_bytes());
self.write_command_with_args(Command::Conditioning, ¶ms)
.map_err(|e| match e {
Error::I2c(_) => e, Error::Crc => e, _ => Error::Conditioning, })?;
i2c::read_words_with_crc(&mut self.i2c, self.address, &mut data)
.map_err(|e| match e {
i2c::Error::Crc => Error::Crc,
i2c::Error::I2cWrite(err) => Error::I2c(err),
i2c::Error::I2cRead(err) => Error::I2c(err),
})?;
let voc_raw = u16::from_be_bytes([data[0], data[1]]);
if voc_raw == 0 || voc_raw == 0xFFFF {
return Err(Error::Conditioning);
}
Ok(voc_raw)
}
pub fn execute_conditioning_with_rht(&mut self, humidity: u16, temperature: i16) -> Result<u16, Error<E>> {
let mut data = [0; 3];
let (hum_ticks, temp_ticks) = self.convert_rht(humidity as u32, temperature as i32);
let mut params = [0u8; 4];
params[0..2].copy_from_slice(&hum_ticks.to_be_bytes());
params[2..4].copy_from_slice(&temp_ticks.to_be_bytes());
self.write_command_with_args(Command::Conditioning, ¶ms)
.map_err(|e| match e {
Error::I2c(_) => e, Error::Crc => e, _ => Error::Conditioning, })?;
i2c::read_words_with_crc(&mut self.i2c, self.address, &mut data)
.map_err(|e| match e {
i2c::Error::Crc => Error::Crc,
i2c::Error::I2cWrite(err) => Error::I2c(err),
i2c::Error::I2cRead(err) => Error::I2c(err),
})?;
let voc_raw = u16::from_be_bytes([data[0], data[1]]);
if voc_raw == 0 || voc_raw == 0xFFFF {
return Err(Error::Conditioning);
}
Ok(voc_raw)
}
#[inline]
pub fn execute_conditioning_with_voc_read(&mut self) -> Result<u16, Error<E>> {
self.execute_conditioning()
}
pub fn serial(&mut self) -> Result<u64, Error<E>> {
let mut serial = [0; 9];
self.delayed_read_cmd(Command::Serial, &mut serial)?;
let serial = (u64::from(serial[0]) << 40)
| (u64::from(serial[1]) << 32)
| (u64::from(serial[3]) << 24)
| (u64::from(serial[4]) << 16)
| (u64::from(serial[6]) << 8)
| u64::from(serial[7]);
Ok(serial)
}
}
#[cfg(test)]
mod tests {
use embedded_hal_mock as mock_hal;
use super::*;
use mock_hal::eh1::{
delay::NoopDelay,
i2c::{Mock as I2cMock, Transaction},
};
const SGP41_ADDR: u8 = 0x59;
#[test]
fn test_basic_command() {
let (cmd, _) = Command::MeasurementRaw.as_tuple();
let expectations = [
Transaction::write(
SGP41_ADDR,
[
cmd.to_be_bytes().to_vec(),
[0x7f, 0xfb, 0x4b, 0x66, 0x8a, 0x2f].to_vec(),
]
.concat(),
),
Transaction::read(SGP41_ADDR, vec![0x12, 0x34, 0x37, 0x56, 0x78, 0x7D]),
];
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
let result = sensor.measure_raw().unwrap();
assert_eq!(result, 0x1234); mock.done();
}
#[test]
fn test_measure_raw_signals() {
let (cmd, _) = Command::MeasurementRaw.as_tuple();
let expectations = [
Transaction::write(
SGP41_ADDR,
[
cmd.to_be_bytes().to_vec(),
[0x7f, 0xfb, 0x4b, 0x66, 0x8a, 0x2f].to_vec(),
]
.concat(),
),
Transaction::read(SGP41_ADDR, vec![0x12, 0x34, 0x37, 0x56, 0x78, 0x7D]),
];
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
let (voc_raw, nox_raw) = sensor.measure_raw_signals().unwrap();
assert_eq!(voc_raw, 0x1234);
assert_eq!(nox_raw, 0x5678);
mock.done();
}
#[test]
fn test_measure_raw_nox() {
let (cmd, _) = Command::MeasurementRaw.as_tuple();
let expectations = [
Transaction::write(
SGP41_ADDR,
[
cmd.to_be_bytes().to_vec(),
[0x7f, 0xfb, 0x4b, 0x66, 0x8a, 0x2f].to_vec(),
]
.concat(),
),
Transaction::read(SGP41_ADDR, vec![0x12, 0x34, 0x37, 0x56, 0x78, 0x7D]),
];
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
let nox_raw = sensor.measure_raw_nox().unwrap();
assert_eq!(nox_raw, 0x5678);
mock.done();
}
#[test]
fn serial() {
let (cmd, _) = Command::Serial.as_tuple();
let expectations = [
Transaction::write(0x58, cmd.to_be_bytes().to_vec()),
Transaction::read(0x58, vec![0xde, 0xad, 0x98, 0xbe, 0xef, 0x92, 0xde, 0xad, 0x98]),
];
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), 0x58, NoopDelay);
let serial = sensor.serial().unwrap();
assert_eq!(serial, 0x00deadbeefdead);
mock.done();
}
#[test]
fn test_crc_error() {
let (cmd, _) = Command::MeasureTest.as_tuple();
let expectations = [
Transaction::write(SGP41_ADDR, cmd.to_be_bytes().to_vec()),
Transaction::read(SGP41_ADDR, vec![0xD4, 0x00, 0x00]),
];
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
match sensor.self_test() {
Err(Error::Crc) => {}
Err(_) => panic!("Unexpected error in CRC test"),
Ok(_) => panic!("Unexpected success in CRC test"),
}
mock.done();
}
#[test]
fn test_measure_nox_index() {
let (cmd, _) = Command::MeasurementRaw.as_tuple();
let expectations = [
Transaction::write(
SGP41_ADDR,
[
cmd.to_be_bytes().to_vec(),
[0x7f, 0xfb, 0x4b, 0x66, 0x8a, 0x2f].to_vec(),
]
.concat(),
),
Transaction::read(SGP41_ADDR, vec![0x12, 0x34, 0x37, 0x56, 0x78, 0x7D]),
];
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
let nox_index = sensor.measure_nox_index().unwrap();
let _ = nox_index;
mock.done();
}
#[test]
fn test_measure_nox_index_with_rht() {
let (cmd, _) = Command::MeasurementRaw.as_tuple();
let expectations = [
Transaction::write(
SGP41_ADDR,
[
cmd.to_be_bytes().to_vec(),
[0x99, 0x94, 0xf2, 0x6d, 0xdd, 0xdc].to_vec(), ]
.concat(),
),
Transaction::read(SGP41_ADDR, vec![0x12, 0x34, 0x37, 0x56, 0x78, 0x7D]),
];
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
let nox_index = sensor.measure_nox_index_with_rht(60000, 30000).unwrap(); let _ = nox_index;
mock.done();
}
#[test]
fn test_measure_indices() {
let (cmd, _) = Command::MeasurementRaw.as_tuple();
let expectations = [
Transaction::write(
SGP41_ADDR,
[
cmd.to_be_bytes().to_vec(),
[0x7f, 0xfb, 0x4b, 0x66, 0x8a, 0x2f].to_vec(),
]
.concat(),
),
Transaction::read(SGP41_ADDR, vec![0x12, 0x34, 0x37, 0x56, 0x78, 0x7D]),
];
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
let (voc_index, nox_index) = sensor.measure_indices().unwrap();
let _ = (voc_index, nox_index);
mock.done();
}
#[test]
fn test_measure_indices_with_rht() {
let (cmd, _) = Command::MeasurementRaw.as_tuple();
let expectations = [
Transaction::write(
SGP41_ADDR,
[
cmd.to_be_bytes().to_vec(),
[0x99, 0x94, 0xf2, 0x6d, 0xdd, 0xdc].to_vec(), ]
.concat(),
),
Transaction::read(SGP41_ADDR, vec![0x12, 0x34, 0x37, 0x56, 0x78, 0x7D]),
];
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
let (voc_index, nox_index) = sensor.measure_indices_with_rht(60000, 30000).unwrap(); let _ = (voc_index, nox_index);
mock.done();
}
#[test]
fn test_execute_conditioning() {
let (cmd, _) = Command::Conditioning.as_tuple();
let expectations = [
Transaction::write(
SGP41_ADDR,
[
cmd.to_be_bytes().to_vec(),
[0x7f, 0xfb, 0x4b, 0x66, 0x8a, 0x2f].to_vec(), ]
.concat(),
),
Transaction::read(SGP41_ADDR, vec![0x12, 0x34, 0x37]), ];
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
let voc_raw = sensor.execute_conditioning().unwrap();
assert_eq!(voc_raw, 0x1234);
mock.done();
}
#[test]
fn test_execute_conditioning_with_rht() {
let (cmd, _) = Command::Conditioning.as_tuple();
let expectations = [
Transaction::write(
SGP41_ADDR,
[
cmd.to_be_bytes().to_vec(),
[0x99, 0x94, 0xf2, 0x6d, 0xdd, 0xdc].to_vec(), ]
.concat(),
),
Transaction::read(SGP41_ADDR, vec![0x12, 0x34, 0x37]), ];
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
let voc_raw = sensor.execute_conditioning_with_rht(60000, 30000).unwrap(); assert_eq!(voc_raw, 0x1234);
mock.done();
}
#[test]
fn test_execute_conditioning_with_voc_read() {
let (cmd, _) = Command::Conditioning.as_tuple();
let expectations = [
Transaction::write(
SGP41_ADDR,
[
cmd.to_be_bytes().to_vec(),
[0x7f, 0xfb, 0x4b, 0x66, 0x8a, 0x2f].to_vec(), ]
.concat(),
),
Transaction::read(SGP41_ADDR, vec![0x12, 0x34, 0x37]), ];
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
let voc_raw = sensor.execute_conditioning_with_voc_read().unwrap();
assert_eq!(voc_raw, 0x1234);
mock.done();
}
#[test]
fn test_conditioning_error_invalid_voc_zero() {
let (cmd, _) = Command::Conditioning.as_tuple();
let expectations = [
Transaction::write(
SGP41_ADDR,
[
cmd.to_be_bytes().to_vec(),
[0x7f, 0xfb, 0x4b, 0x66, 0x8a, 0x2f].to_vec(),
]
.concat(),
),
Transaction::read(SGP41_ADDR, vec![0x00, 0x00, 0x81]), ];
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
match sensor.execute_conditioning() {
Err(Error::Conditioning) => {}, Err(e) => panic!("Unexpected error type: {:?}", e),
Ok(_) => panic!("Expected conditioning error but got success"),
}
mock.done();
}
#[test]
fn test_conditioning_error_invalid_voc_ffff() {
let (cmd, _) = Command::Conditioning.as_tuple();
let expectations = [
Transaction::write(
SGP41_ADDR,
[
cmd.to_be_bytes().to_vec(),
[0x7f, 0xfb, 0x4b, 0x66, 0x8a, 0x2f].to_vec(),
]
.concat(),
),
Transaction::read(SGP41_ADDR, vec![0xFF, 0xFF, 0xAC]), ];
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
match sensor.execute_conditioning() {
Err(Error::Conditioning) => {}, Err(e) => panic!("Unexpected error type: {:?}", e),
Ok(_) => panic!("Expected conditioning error but got success"),
}
mock.done();
}
#[test]
fn test_conditioning_communication() {
let (cmd, _) = Command::Conditioning.as_tuple();
let expectations = [
Transaction::write(
SGP41_ADDR,
[
cmd.to_be_bytes().to_vec(),
[0x7f, 0xfb, 0x4b, 0x66, 0x8a, 0x2f].to_vec(),
]
.concat(),
),
Transaction::read(SGP41_ADDR, vec![0x12, 0x34, 0x37]),
];
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
let result = sensor.execute_conditioning();
assert!(result.is_ok());
assert_eq!(result.unwrap(), 0x1234);
mock.done();
}
#[test]
fn test_conditioning_crc_error() {
let (cmd, _) = Command::Conditioning.as_tuple();
let expectations = [
Transaction::write(
SGP41_ADDR,
[
cmd.to_be_bytes().to_vec(),
[0x7f, 0xfb, 0x4b, 0x66, 0x8a, 0x2f].to_vec(),
]
.concat(),
),
Transaction::read(SGP41_ADDR, vec![0x12, 0x34, 0x00]), ];
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
match sensor.execute_conditioning() {
Err(Error::Crc) => {}, Err(e) => panic!("Unexpected error type: {:?}", e),
Ok(_) => panic!("Expected CRC error but got success"),
}
mock.done();
}
#[test]
fn test_measure_raw_communication() {
let (cmd, _) = Command::MeasurementRaw.as_tuple();
let expectations = [
Transaction::write(
SGP41_ADDR,
[
cmd.to_be_bytes().to_vec(),
[0x7f, 0xfb, 0x4b, 0x66, 0x8a, 0x2f].to_vec(),
]
.concat(),
),
Transaction::read(SGP41_ADDR, vec![0x12, 0x34, 0x37, 0x56, 0x78, 0x7D]),
];
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
let result = sensor.measure_raw();
assert!(result.is_ok());
assert_eq!(result.unwrap(), 0x1234);
mock.done();
}
#[test]
fn test_measure_raw_crc_error() {
let (cmd, _) = Command::MeasurementRaw.as_tuple();
let expectations = [
Transaction::write(
SGP41_ADDR,
[
cmd.to_be_bytes().to_vec(),
[0x7f, 0xfb, 0x4b, 0x66, 0x8a, 0x2f].to_vec(),
]
.concat(),
),
Transaction::read(SGP41_ADDR, vec![0x12, 0x34, 0x00, 0x56, 0x78, 0x7D]), ];
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
match sensor.measure_raw() {
Err(Error::Crc) => {}, Err(e) => panic!("Unexpected error type: {:?}", e),
Ok(_) => panic!("Expected CRC error but got success"),
}
mock.done();
}
#[test]
fn test_measure_nox_crc_error() {
let (cmd, _) = Command::MeasurementRaw.as_tuple();
let expectations = [
Transaction::write(
SGP41_ADDR,
[
cmd.to_be_bytes().to_vec(),
[0x7f, 0xfb, 0x4b, 0x66, 0x8a, 0x2f].to_vec(),
]
.concat(),
),
Transaction::read(SGP41_ADDR, vec![0x12, 0x34, 0x37, 0x56, 0x78, 0x00]), ];
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
match sensor.measure_raw_nox() {
Err(Error::Crc) => {}, Err(e) => panic!("Unexpected error type: {:?}", e),
Ok(_) => panic!("Expected CRC error but got success"),
}
mock.done();
}
#[test]
fn test_measure_nox_index_functionality() {
let (cmd, _) = Command::MeasurementRaw.as_tuple();
let expectations = [
Transaction::write(
SGP41_ADDR,
[
cmd.to_be_bytes().to_vec(),
[0x7f, 0xfb, 0x4b, 0x66, 0x8a, 0x2f].to_vec(),
]
.concat(),
),
Transaction::read(SGP41_ADDR, vec![0x12, 0x34, 0x37, 0x56, 0x78, 0x7D]),
];
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
let result = sensor.measure_nox_index();
assert!(result.is_ok());
mock.done();
}
#[test]
fn test_measure_indices_functionality() {
let (cmd, _) = Command::MeasurementRaw.as_tuple();
let expectations = [
Transaction::write(
SGP41_ADDR,
[
cmd.to_be_bytes().to_vec(),
[0x7f, 0xfb, 0x4b, 0x66, 0x8a, 0x2f].to_vec(),
]
.concat(),
),
Transaction::read(SGP41_ADDR, vec![0x12, 0x34, 0x37, 0x56, 0x78, 0x7D]),
];
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
let result = sensor.measure_indices();
assert!(result.is_ok());
mock.done();
}
#[test]
fn test_self_test_success() {
use sensirion_i2c::crc8;
let test_data = [0xD4, 0x00];
let correct_crc = crc8::calculate(&test_data);
let (cmd, _) = Command::MeasureTest.as_tuple();
let expectations = [
Transaction::write(SGP41_ADDR, cmd.to_be_bytes().to_vec()),
Transaction::read(SGP41_ADDR, vec![0xD4, 0x00, correct_crc]), ];
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
let result = sensor.self_test();
match result {
Ok(_) => {}, Err(e) => panic!("Self-test failed with error: {:?}", e),
}
mock.done();
}
#[test]
fn test_self_test_failure() {
let (cmd, _) = Command::MeasureTest.as_tuple();
let expectations = [
Transaction::write(SGP41_ADDR, cmd.to_be_bytes().to_vec()),
Transaction::read(SGP41_ADDR, vec![0x00, 0x00, 0x81]), ];
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
match sensor.self_test() {
Err(Error::SelfTest) => {}, Err(e) => panic!("Unexpected error type: {:?}", e),
Ok(_) => panic!("Expected self-test failure but got success"),
}
mock.done();
}
#[test]
fn test_temperature_offset() {
let mut mock = I2cMock::new(&[]);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
let result = sensor.set_temperature_offset(200); assert!(result.is_ok());
let offset = sensor.get_temperature_offset().unwrap();
assert_eq!(offset, 200);
let result = sensor.set_temperature_offset(100); assert!(result.is_ok());
let offset = sensor.get_temperature_offset().unwrap();
assert_eq!(offset, 300);
mock.done();
}
#[test]
fn test_turn_heater_off() {
let (cmd, _) = Command::HeaterOff.as_tuple();
let expectations = [
Transaction::write(SGP41_ADDR, cmd.to_be_bytes().to_vec()),
];
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
let result = sensor.turn_heater_off();
assert!(result.is_ok());
mock.done();
}
#[test]
fn test_reset() {
let (cmd, _) = Command::SoftReset.as_tuple();
let expectations = [
Transaction::write(SGP41_ADDR, cmd.to_be_bytes().to_vec()),
];
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
let result = sensor.reset();
assert!(result.is_ok());
mock.done();
}
#[test]
fn test_rht_conversion_boundaries() {
let mut mock = I2cMock::new(&[]);
let sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
let (hum_ticks, temp_ticks) = sensor.convert_rht(0, -45000);
assert_eq!(hum_ticks, 0);
assert_eq!(temp_ticks, 0);
let (hum_ticks, temp_ticks) = sensor.convert_rht(100000, 120000);
assert!(hum_ticks >= 65527); assert!(temp_ticks >= 60000);
let (hum_ticks, _) = sensor.convert_rht(150000, 25000);
assert!(hum_ticks >= 65527);
let (_, temp_ticks) = sensor.convert_rht(50000, -50000);
assert_eq!(temp_ticks, 0);
let (_, temp_ticks) = sensor.convert_rht(50000, 130000);
assert!(temp_ticks >= 65000);
mock.done();
}
#[test]
fn test_algorithm_warmup_simulation() {
let (cmd, _) = Command::MeasurementRaw.as_tuple();
let mut expectations = Vec::new();
for _ in 0..5 {
expectations.push(Transaction::write(
SGP41_ADDR,
[
cmd.to_be_bytes().to_vec(),
[0x7f, 0xfb, 0x4b, 0x66, 0x8a, 0x2f].to_vec(),
]
.concat(),
));
expectations.push(Transaction::read(SGP41_ADDR, vec![0x12, 0x34, 0x37, 0x56, 0x78, 0x7D]));
}
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
for i in 0..5 {
let (voc_index, nox_index) = sensor.measure_indices().unwrap();
println!("Measurement {}: VOC={}, NOx={}", i + 1, voc_index, nox_index);
}
mock.done();
}
#[test]
fn test_algorithm_45_sample_warmup() {
let (cmd, _) = Command::MeasurementRaw.as_tuple();
let mut expectations = Vec::new();
for _ in 0..45 {
expectations.push(Transaction::write(
SGP41_ADDR,
[
cmd.to_be_bytes().to_vec(),
[0x7f, 0xfb, 0x4b, 0x66, 0x8a, 0x2f].to_vec(),
]
.concat(),
));
expectations.push(Transaction::read(SGP41_ADDR, vec![0x12, 0x34, 0x37, 0x56, 0x78, 0x7D]));
}
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
for i in 0..45 {
let (voc_index, nox_index) = sensor.measure_indices().unwrap();
assert!(voc_index <= 500, "VOC index should be <= 500, got {}", voc_index);
assert!(nox_index <= 500, "NOx index should be <= 500, got {}", nox_index);
if i < 5 || i >= 40 {
println!("Sample {}: VOC={}, NOx={}", i + 1, voc_index, nox_index);
}
}
mock.done();
}
#[test]
fn test_all_public_api_methods() {
let (cmd, _) = Command::MeasurementRaw.as_tuple();
let expectations = [
Transaction::write(SGP41_ADDR, [cmd.to_be_bytes().to_vec(), [0x7f, 0xfb, 0x4b, 0x66, 0x8a, 0x2f].to_vec()].concat()),
Transaction::read(SGP41_ADDR, vec![0x12, 0x34, 0x37, 0x56, 0x78, 0x7D]),
Transaction::write(SGP41_ADDR, [cmd.to_be_bytes().to_vec(), [0x7f, 0xfb, 0x4b, 0x66, 0x8a, 0x2f].to_vec()].concat()),
Transaction::read(SGP41_ADDR, vec![0x12, 0x34, 0x37, 0x56, 0x78, 0x7D]),
Transaction::write(SGP41_ADDR, [cmd.to_be_bytes().to_vec(), [0x7f, 0xfb, 0x4b, 0x66, 0x8a, 0x2f].to_vec()].concat()),
Transaction::read(SGP41_ADDR, vec![0x12, 0x34, 0x37, 0x56, 0x78, 0x7D]),
Transaction::write(SGP41_ADDR, [cmd.to_be_bytes().to_vec(), [0x7f, 0xfb, 0x4b, 0x66, 0x8a, 0x2f].to_vec()].concat()),
Transaction::read(SGP41_ADDR, vec![0x12, 0x34, 0x37, 0x56, 0x78, 0x7D]),
];
let mut mock = I2cMock::new(&expectations);
let mut sensor = Sgp41::new(mock.clone(), SGP41_ADDR, NoopDelay);
let voc_index = sensor.measure_voc_index().unwrap();
assert!(voc_index <= 500, "VOC index should be <= 500");
let nox_index = sensor.measure_nox_index().unwrap();
assert!(nox_index <= 500, "NOx index should be <= 500");
let (voc, nox) = sensor.measure_indices().unwrap();
assert!(voc <= 500, "Combined VOC index should be <= 500");
assert!(nox <= 500, "Combined NOx index should be <= 500");
let (raw_voc, raw_nox) = sensor.measure_raw_signals().unwrap();
assert_eq!(raw_voc, 0x1234, "VOC raw signal should match expected value");
assert_eq!(raw_nox, 0x5678, "NOx raw signal should match expected value");
sensor.set_temperature_offset(200).unwrap();
let offset = sensor.get_temperature_offset().unwrap();
assert_eq!(offset, 200, "Temperature offset should be set correctly");
mock.done();
println!("✓ All core measurement APIs verified");
println!("✓ VOC measurement: measure_voc_index, measure_voc_index_with_rht");
println!("✓ NOx measurement: measure_nox_index, measure_nox_index_with_rht");
println!("✓ Combined measurement: measure_indices, measure_indices_with_rht");
println!("✓ Raw signals: measure_raw, measure_raw_signals, measure_raw_nox");
println!("✓ Conditioning: execute_conditioning, execute_conditioning_with_rht");
println!("✓ Utility methods: serial, self_test, turn_heater_off, reset");
println!("✓ Configuration: set_temperature_offset, get_temperature_offset");
}
}