use embedded_hal as hal;
use hal::delay::DelayNs;
use hal::i2c::I2c;
use crate::commands::Command;
use crate::error::Error;
use crate::types::{RawSensorData, SensorData};
use sensirion_i2c::{crc8, i2c};
const SCD4X_I2C_ADDRESS: u8 = 0x62;
#[cfg(feature = "embedded-hal-async")]
mod async_impl;
#[cfg(feature = "embedded-hal-async")]
pub use async_impl::Scd4xAsync;
#[derive(Debug, Default)]
pub struct Scd4x<I2C, D> {
i2c: I2C,
delay: D,
is_running: bool,
}
impl<I2C, D, E> Scd4x<I2C, D>
where
I2C: I2c<Error = E>,
D: DelayNs,
{
pub fn new(i2c: I2C, delay: D) -> Self {
Scd4x {
i2c,
delay,
is_running: false,
}
}
pub fn destroy(self) -> I2C {
self.i2c
}
pub fn start_periodic_measurement(&mut self) -> Result<(), Error<E>> {
self.write_command(Command::StartPeriodicMeasurement)?;
self.is_running = true;
Ok(())
}
pub fn stop_periodic_measurement(&mut self) -> Result<(), Error<E>> {
self.write_command(Command::StopPeriodicMeasurement)?;
self.is_running = false;
Ok(())
}
pub fn sensor_output(&mut self) -> Result<RawSensorData, Error<E>> {
let mut buf = [0; 9];
self.delayed_read_cmd(Command::ReadMeasurement, &mut buf)?;
Ok(RawSensorData::from_bytes(buf))
}
pub fn measurement(&mut self) -> Result<SensorData, Error<E>> {
let raw = self.sensor_output()?;
Ok(SensorData::from_raw(raw))
}
pub fn temperature_offset(&mut self) -> Result<f32, Error<E>> {
let mut buf = [0; 3];
self.delayed_read_cmd(Command::GetTemperatureOffset, &mut buf)?;
Ok(temp_offset_from_bytes(buf))
}
pub fn set_temperature_offset(&mut self, offset: f32) -> Result<(), Error<E>> {
let t_offset = temp_offset_to_u16(offset);
self.write_command_with_data(Command::SetTemperatureOffset, t_offset)?;
Ok(())
}
pub fn altitude(&mut self) -> Result<u16, Error<E>> {
let mut buf = [0; 3];
self.delayed_read_cmd(Command::GetSensorAltitude, &mut buf)?;
let altitude = u16::from_be_bytes([buf[0], buf[1]]);
Ok(altitude)
}
pub fn set_altitude(&mut self, altitude: u16) -> Result<(), Error<E>> {
self.write_command_with_data(Command::SetSensorAltitude, altitude)?;
Ok(())
}
pub fn set_ambient_pressure(&mut self, pressure_hpa: u16) -> Result<(), Error<E>> {
self.write_command_with_data(Command::SetAmbientPressure, pressure_hpa)?;
Ok(())
}
pub fn forced_recalibration(&mut self, target_co2_concentration: u16) -> Result<u16, Error<E>> {
let frc_correction = self.delayed_read_cmd_with_data(
Command::PerformForcedRecalibration,
target_co2_concentration,
)?;
check_frc_correction(frc_correction)
}
pub fn automatic_self_calibration(&mut self) -> Result<bool, Error<E>> {
let mut buf = [0; 3];
self.delayed_read_cmd(Command::GetAutomaticSelfCalibrationEnabled, &mut buf)?;
let status = u16::from_be_bytes([buf[0], buf[1]]) != 0;
Ok(status)
}
pub fn set_automatic_self_calibration(&mut self, enabled: bool) -> Result<(), Error<E>> {
self.write_command_with_data(Command::SetAutomaticSelfCalibrationEnabled, enabled as u16)?;
Ok(())
}
pub fn automatic_self_calibration_target(&mut self) -> Result<u16, Error<E>> {
let mut buf = [0; 3];
self.delayed_read_cmd(Command::GetAutomaticSelfCalibrationTarget, &mut buf)?;
let ppm = u16::from_be_bytes([buf[0], buf[1]]);
Ok(ppm)
}
pub fn set_automatic_self_calibration_target(&mut self, ppm: u16) -> Result<(), Error<E>> {
self.write_command_with_data(Command::SetAutomaticSelfCalibrationTarget, ppm)?;
Ok(())
}
pub fn start_low_power_periodic_measurements(&mut self) -> Result<(), Error<E>> {
self.write_command(Command::StartLowPowerPeriodicMeasurement)?;
Ok(())
}
pub fn data_ready_status(&mut self) -> Result<bool, Error<E>> {
let mut buf = [0; 3];
self.delayed_read_cmd(Command::GetDataReadyStatus, &mut buf)?;
let status = u16::from_be_bytes([buf[0], buf[1]]);
let ready = (status & 0x7FF) != 0;
Ok(ready)
}
pub fn persist_settings(&mut self) -> Result<(), Error<E>> {
self.write_command(Command::PersistSettings)?;
Ok(())
}
pub fn serial_number(&mut self) -> Result<u64, Error<E>> {
let mut buf = [0; 9];
self.delayed_read_cmd(Command::GetSerialNumber, &mut buf)?;
Ok(serial_number_from_bytes(buf))
}
pub fn self_test_is_ok(&mut self) -> Result<bool, Error<E>> {
let mut buf = [0; 3];
self.delayed_read_cmd(Command::PerformSelfTest, &mut buf)?;
let status = u16::from_be_bytes([buf[0], buf[1]]) == 0;
Ok(status)
}
pub fn factory_reset(&mut self) -> Result<(), Error<E>> {
self.write_command(Command::PerformFactoryReset)?;
Ok(())
}
pub fn reinit(&mut self) -> Result<(), Error<E>> {
self.write_command(Command::Reinit)?;
Ok(())
}
#[cfg(feature = "scd41")]
pub fn measure_single_shot(&mut self) -> Result<(), Error<E>> {
self.write_command(Command::MeasureSingleShot)?;
Ok(())
}
#[cfg(feature = "scd41")]
pub fn measure_single_shot_non_blocking(&mut self) -> Result<(), Error<E>> {
self.write_command(Command::MeasureSingleShotNonBlocking)?;
Ok(())
}
#[cfg(feature = "scd41")]
pub fn measure_single_shot_rht(&mut self) -> Result<(), Error<E>> {
self.write_command(Command::MeasureSingleShotRhtOnly)?;
Ok(())
}
#[cfg(feature = "scd41")]
pub fn power_down(&mut self) -> Result<(), Error<E>> {
self.write_command(Command::PowerDown)?;
Ok(())
}
#[cfg(feature = "scd41")]
pub fn wake_up(&mut self) {
self.write_command(Command::WakeUp).ok();
}
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, SCD4X_I2C_ADDRESS, data)?;
Ok(())
}
fn delayed_read_cmd_with_data(&mut self, cmd: Command, data: u16) -> Result<u16, Error<E>> {
self.write_command_with_data(cmd, data)?;
let mut buf = [0; 3];
i2c::read_words_with_crc(&mut self.i2c, SCD4X_I2C_ADDRESS, &mut buf)?;
Ok(u16::from_be_bytes([buf[0], buf[1]]))
}
fn write_command(&mut self, cmd: Command) -> Result<(), Error<E>> {
let (command, delay, allowed_if_running) = cmd.as_tuple();
if !allowed_if_running && self.is_running {
return Err(Error::NotAllowed);
}
i2c::write_command_u16(&mut self.i2c, SCD4X_I2C_ADDRESS, command).map_err(Error::I2c)?;
self.delay.delay_ms(delay);
Ok(())
}
fn write_command_with_data(&mut self, cmd: Command, data: u16) -> Result<(), Error<E>> {
let (command, delay, allowed_if_running) = cmd.as_tuple();
if !allowed_if_running && self.is_running {
return Err(Error::NotAllowed);
}
let buf = encode_cmd_with_data(command, data);
self.i2c
.write(SCD4X_I2C_ADDRESS, &buf)
.map_err(Error::I2c)?;
self.delay.delay_ms(delay);
Ok(())
}
}
impl RawSensorData {
fn from_bytes(buf: [u8; 9]) -> Self {
let co2 = u16::from_be_bytes([buf[0], buf[1]]);
let temperature = u16::from_be_bytes([buf[3], buf[4]]);
let humidity = u16::from_be_bytes([buf[6], buf[7]]);
Self {
co2,
temperature,
humidity,
}
}
}
impl SensorData {
fn from_raw(raw: RawSensorData) -> Self {
let RawSensorData {
co2,
temperature,
humidity,
} = raw;
SensorData {
co2,
temperature: (temperature as f32 * 175_f32) / (u16::MAX as f32) - 45_f32,
humidity: (humidity as f32 * 100_f32) / (u16::MAX as f32),
}
}
}
fn serial_number_from_bytes(buf: [u8; 9]) -> u64 {
(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])
}
fn encode_cmd_with_data(command: u16, data: u16) -> [u8; 5] {
let c = command.to_be_bytes();
let d = data.to_be_bytes();
let mut buf = [0; 5];
buf[0..2].copy_from_slice(&c);
buf[2..4].copy_from_slice(&d);
buf[4] = crc8::calculate(&d);
buf
}
fn temp_offset_from_bytes(buf: [u8; 3]) -> f32 {
let raw_offset = u16::from_be_bytes([buf[0], buf[1]]);
(raw_offset as f32 * 175_f32) / (u16::MAX as f32)
}
fn temp_offset_to_u16(offset: f32) -> u16 {
(((offset * (u16::MAX as f32)) / 175_f32) as i32) as u16
}
fn check_frc_correction<E>(frc_correction: u16) -> Result<u16, Error<E>> {
if frc_correction == u16::MAX {
return Err(Error::Internal);
}
match frc_correction.checked_sub(0x8000) {
Some(concentration) => Ok(concentration),
None => Err(Error::Internal),
}
}
#[cfg(test)]
mod tests {
use embedded_hal_mock::eh1 as hal;
use self::hal::delay::NoopDelay as DelayMock;
use self::hal::i2c::{Mock as I2cMock, Transaction};
use super::*;
fn word(msb: u8, lsb: u8) -> [u8; 3] {
[msb, lsb, crc8::calculate(&[msb, lsb])]
}
#[test]
fn test_get_serial_number() {
let (cmd, _, _) = Command::GetSerialNumber.as_tuple();
let expectations = [
Transaction::write(SCD4X_I2C_ADDRESS, cmd.to_be_bytes().to_vec()),
Transaction::read(
SCD4X_I2C_ADDRESS,
vec![0xbe, 0xef, 0x92, 0xbe, 0xef, 0x92, 0xbe, 0xef, 0x92],
),
];
let mock = I2cMock::new(&expectations);
let mut sensor = Scd4x::new(mock, DelayMock);
let serial = sensor.serial_number().unwrap();
assert_eq!(serial, 0xbeefbeefbeef);
let mut mock = sensor.destroy();
mock.done();
}
#[test]
fn test_measurement() {
let (cmd, _, _) = Command::ReadMeasurement.as_tuple();
let expectations = [
Transaction::write(SCD4X_I2C_ADDRESS, cmd.to_be_bytes().to_vec()),
Transaction::read(
SCD4X_I2C_ADDRESS,
vec![0x03, 0xE8, 0xD4, 0x62, 0x03, 0x5E, 0x80, 0x00, 0xA2],
),
];
let mock = I2cMock::new(&expectations);
let mut sensor = Scd4x::new(mock, DelayMock);
let data = sensor.measurement().unwrap();
assert_eq!(data.co2, 1000_u16);
assert_eq!(data.temperature, 22.00122_f32);
assert_eq!(data.humidity, 50.000763_f32);
let mut mock = sensor.destroy();
mock.done();
}
#[test]
fn test_temp_offset_round_trip() {
let offsets = [0.0_f32, 1.0, 5.0, 10.5, 0.1];
for original in offsets {
let encoded = temp_offset_to_u16(original);
let buf = [
(encoded >> 8) as u8,
encoded as u8,
0, ];
let decoded = temp_offset_from_bytes(buf);
assert!(
(original - decoded).abs() < 0.01,
"Round-trip failed for {original}: got {decoded}"
);
}
}
#[test]
fn test_not_allowed_when_running() {
let (start_cmd, _, _) = Command::StartPeriodicMeasurement.as_tuple();
let expectations = [Transaction::write(
SCD4X_I2C_ADDRESS,
start_cmd.to_be_bytes().to_vec(),
)];
let mock = I2cMock::new(&expectations);
let mut sensor = Scd4x::new(mock, DelayMock);
sensor.start_periodic_measurement().unwrap();
assert_eq!(sensor.serial_number(), Err(Error::NotAllowed));
assert_eq!(sensor.temperature_offset(), Err(Error::NotAllowed));
assert_eq!(sensor.self_test_is_ok(), Err(Error::NotAllowed));
assert_eq!(sensor.factory_reset(), Err(Error::NotAllowed));
assert_eq!(sensor.reinit(), Err(Error::NotAllowed));
assert_eq!(sensor.persist_settings(), Err(Error::NotAllowed));
let mut mock = sensor.destroy();
mock.done();
}
#[test]
fn test_allowed_when_running() {
let (start_cmd, _, _) = Command::StartPeriodicMeasurement.as_tuple();
let (read_cmd, _, _) = Command::ReadMeasurement.as_tuple();
let (ready_cmd, _, _) = Command::GetDataReadyStatus.as_tuple();
let ready_word = word(0x80, 0x01);
let co2_word = word(0x01, 0xF4);
let temp_word = word(0x62, 0x03);
let hum_word = word(0x80, 0x00);
let expectations = [
Transaction::write(SCD4X_I2C_ADDRESS, start_cmd.to_be_bytes().to_vec()),
Transaction::write(SCD4X_I2C_ADDRESS, ready_cmd.to_be_bytes().to_vec()),
Transaction::read(SCD4X_I2C_ADDRESS, ready_word.to_vec()),
Transaction::write(SCD4X_I2C_ADDRESS, read_cmd.to_be_bytes().to_vec()),
Transaction::read(SCD4X_I2C_ADDRESS, [co2_word, temp_word, hum_word].concat()),
];
let mock = I2cMock::new(&expectations);
let mut sensor = Scd4x::new(mock, DelayMock);
sensor.start_periodic_measurement().unwrap();
assert!(sensor.data_ready_status().unwrap());
let data = sensor.measurement().unwrap();
assert_eq!(data.co2, 500);
let mut mock = sensor.destroy();
mock.done();
}
#[test]
fn test_data_not_ready() {
let (cmd, _, _) = Command::GetDataReadyStatus.as_tuple();
let w = word(0xF8, 0x00);
let expectations = [
Transaction::write(SCD4X_I2C_ADDRESS, cmd.to_be_bytes().to_vec()),
Transaction::read(SCD4X_I2C_ADDRESS, w.to_vec()),
];
let mock = I2cMock::new(&expectations);
let mut sensor = Scd4x::new(mock, DelayMock);
assert!(!sensor.data_ready_status().unwrap());
let mut mock = sensor.destroy();
mock.done();
}
#[test]
fn test_data_ready() {
let (cmd, _, _) = Command::GetDataReadyStatus.as_tuple();
let w = word(0x00, 0x01);
let expectations = [
Transaction::write(SCD4X_I2C_ADDRESS, cmd.to_be_bytes().to_vec()),
Transaction::read(SCD4X_I2C_ADDRESS, w.to_vec()),
];
let mock = I2cMock::new(&expectations);
let mut sensor = Scd4x::new(mock, DelayMock);
assert!(sensor.data_ready_status().unwrap());
let mut mock = sensor.destroy();
mock.done();
}
#[test]
fn test_frc_correction_valid() {
let result = check_frc_correction::<()>(0x8032);
assert_eq!(result, Ok(50));
}
#[test]
fn test_frc_correction_zero() {
let result = check_frc_correction::<()>(0x8000);
assert_eq!(result, Ok(0));
}
#[test]
fn test_frc_correction_failed() {
let result = check_frc_correction::<()>(0xFFFF);
assert_eq!(result, Err(Error::Internal));
}
#[test]
fn test_frc_correction_underflow() {
let result = check_frc_correction::<()>(0x7FFF);
assert_eq!(result, Err(Error::Internal));
}
#[test]
fn test_self_test_pass() {
let (cmd, _, _) = Command::PerformSelfTest.as_tuple();
let w = word(0x00, 0x00);
let expectations = [
Transaction::write(SCD4X_I2C_ADDRESS, cmd.to_be_bytes().to_vec()),
Transaction::read(SCD4X_I2C_ADDRESS, w.to_vec()),
];
let mock = I2cMock::new(&expectations);
let mut sensor = Scd4x::new(mock, DelayMock);
assert!(sensor.self_test_is_ok().unwrap());
let mut mock = sensor.destroy();
mock.done();
}
#[test]
fn test_self_test_fail() {
let (cmd, _, _) = Command::PerformSelfTest.as_tuple();
let w = word(0x00, 0x01);
let expectations = [
Transaction::write(SCD4X_I2C_ADDRESS, cmd.to_be_bytes().to_vec()),
Transaction::read(SCD4X_I2C_ADDRESS, w.to_vec()),
];
let mock = I2cMock::new(&expectations);
let mut sensor = Scd4x::new(mock, DelayMock);
assert!(!sensor.self_test_is_ok().unwrap());
let mut mock = sensor.destroy();
mock.done();
}
#[test]
fn test_stop_clears_running() {
let (start_cmd, _, _) = Command::StartPeriodicMeasurement.as_tuple();
let (stop_cmd, _, _) = Command::StopPeriodicMeasurement.as_tuple();
let (serial_cmd, _, _) = Command::GetSerialNumber.as_tuple();
let w1 = word(0x00, 0x01);
let w2 = word(0x00, 0x02);
let w3 = word(0x00, 0x03);
let expectations = [
Transaction::write(SCD4X_I2C_ADDRESS, start_cmd.to_be_bytes().to_vec()),
Transaction::write(SCD4X_I2C_ADDRESS, stop_cmd.to_be_bytes().to_vec()),
Transaction::write(SCD4X_I2C_ADDRESS, serial_cmd.to_be_bytes().to_vec()),
Transaction::read(SCD4X_I2C_ADDRESS, [w1, w2, w3].concat()),
];
let mock = I2cMock::new(&expectations);
let mut sensor = Scd4x::new(mock, DelayMock);
sensor.start_periodic_measurement().unwrap();
assert_eq!(sensor.serial_number(), Err(Error::NotAllowed));
sensor.stop_periodic_measurement().unwrap();
assert!(sensor.serial_number().is_ok());
let mut mock = sensor.destroy();
mock.done();
}
#[test]
fn test_get_altitude() {
let (cmd, _, _) = Command::GetSensorAltitude.as_tuple();
let w = word(0x02, 0x58);
let expectations = [
Transaction::write(SCD4X_I2C_ADDRESS, cmd.to_be_bytes().to_vec()),
Transaction::read(SCD4X_I2C_ADDRESS, w.to_vec()),
];
let mock = I2cMock::new(&expectations);
let mut sensor = Scd4x::new(mock, DelayMock);
assert_eq!(sensor.altitude().unwrap(), 600);
let mut mock = sensor.destroy();
mock.done();
}
#[test]
fn test_asc_enabled() {
let (cmd, _, _) = Command::GetAutomaticSelfCalibrationEnabled.as_tuple();
let w = word(0x00, 0x01);
let expectations = [
Transaction::write(SCD4X_I2C_ADDRESS, cmd.to_be_bytes().to_vec()),
Transaction::read(SCD4X_I2C_ADDRESS, w.to_vec()),
];
let mock = I2cMock::new(&expectations);
let mut sensor = Scd4x::new(mock, DelayMock);
assert!(sensor.automatic_self_calibration().unwrap());
let mut mock = sensor.destroy();
mock.done();
}
#[test]
fn test_asc_disabled() {
let (cmd, _, _) = Command::GetAutomaticSelfCalibrationEnabled.as_tuple();
let w = word(0x00, 0x00);
let expectations = [
Transaction::write(SCD4X_I2C_ADDRESS, cmd.to_be_bytes().to_vec()),
Transaction::read(SCD4X_I2C_ADDRESS, w.to_vec()),
];
let mock = I2cMock::new(&expectations);
let mut sensor = Scd4x::new(mock, DelayMock);
assert!(!sensor.automatic_self_calibration().unwrap());
let mut mock = sensor.destroy();
mock.done();
}
}