sht4x-rs 0.1.0

Sensirion SHT4x temperature & humidity sensor driver (embedded-hal 1.0, no_std, blocking + async)
Documentation
#![no_std]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![deny(missing_docs)]
//! `embedded-hal` 1.0 driver for the Sensirion SHT4x family
//! (SHT40, SHT41, SHT45) temperature & humidity sensors.
//!
//! Supports both blocking and `async` APIs behind cargo features.
//!
//! ```no_run
//! # #[cfg(feature = "blocking")]
//! # fn run<I, D>(i2c: I, delay: D) -> Result<(), sht4x::Error<I::Error>>
//! # where I: embedded_hal::i2c::I2c, D: embedded_hal::delay::DelayNs {
//! use sht4x::{Sht4x, Precision, DEFAULT_ADDRESS};
//! let mut sensor = Sht4x::new(i2c, delay, DEFAULT_ADDRESS);
//! let m = sensor.measure(Precision::High)?;
//! let _ = (m.temperature_celsius(), m.humidity_percent());
//! # Ok(()) }
//! ```

pub mod commands;
mod crc;
mod error;
mod types;

pub use error::Error;
pub use types::{HeaterDuration, HeaterPower, Measurement, Precision};

/// Default I²C address for the SHT4x family.
pub const DEFAULT_ADDRESS: u8 = 0x44;

/// Length of a measurement read (T word + CRC + RH word + CRC).
const MEASUREMENT_LEN: usize = 6;
/// Length of a serial-number read (2 words, each with CRC).
const SERIAL_LEN: usize = 6;

/// SHT4x driver, generic over an I²C bus and a delay source.
///
/// Construct with [`Sht4x::new`]. The same struct provides either the
/// blocking API (default feature `blocking`) or the async API
/// (feature `async`), depending on which `embedded-hal` traits the
/// provided `I2C` / `D` implement.
pub struct Sht4x<I2C, D> {
    i2c: I2C,
    delay: D,
    address: u8,
}

impl<I2C, D> Sht4x<I2C, D> {
    /// Create a new driver instance.
    ///
    /// `address` is usually [`DEFAULT_ADDRESS`] (0x44) unless you have a
    /// part variant with a non-standard address.
    pub fn new(i2c: I2C, delay: D, address: u8) -> Self {
        Self {
            i2c,
            delay,
            address,
        }
    }

    /// Release the bus and delay back to the caller.
    pub fn release(self) -> (I2C, D) {
        (self.i2c, self.delay)
    }
}

/// Decode a 6-byte `[t_hi, t_lo, t_crc, h_hi, h_lo, h_crc]` reply.
fn decode_measurement<E>(buf: &[u8; MEASUREMENT_LEN]) -> Result<Measurement, Error<E>> {
    let t = [buf[0], buf[1], buf[2]];
    let h = [buf[3], buf[4], buf[5]];
    if !crc::verify_word(&t) || !crc::verify_word(&h) {
        return Err(Error::Crc);
    }
    let raw_t = u16::from_be_bytes([t[0], t[1]]);
    let raw_h = u16::from_be_bytes([h[0], h[1]]);
    Ok(Measurement::from_raw(raw_t, raw_h))
}

/// Decode a 6-byte serial-number reply into a `u32`.
fn decode_serial<E>(buf: &[u8; SERIAL_LEN]) -> Result<u32, Error<E>> {
    let w0 = [buf[0], buf[1], buf[2]];
    let w1 = [buf[3], buf[4], buf[5]];
    if !crc::verify_word(&w0) || !crc::verify_word(&w1) {
        return Err(Error::Crc);
    }
    let hi = u16::from_be_bytes([w0[0], w0[1]]);
    let lo = u16::from_be_bytes([w1[0], w1[1]]);
    Ok(((hi as u32) << 16) | lo as u32)
}

#[cfg(feature = "blocking")]
mod blocking_impl {
    use super::*;
    use embedded_hal::delay::DelayNs;
    use embedded_hal::i2c::I2c;

    impl<I2C, D> Sht4x<I2C, D>
    where
        I2C: I2c,
        D: DelayNs,
    {
        /// Trigger a measurement and read back temperature + humidity.
        pub fn measure(&mut self, precision: Precision) -> Result<Measurement, Error<I2C::Error>> {
            self.i2c
                .write(self.address, &[precision.command()])
                .map_err(Error::I2c)?;
            self.delay.delay_us(precision.duration_us());
            let mut buf = [0u8; MEASUREMENT_LEN];
            self.i2c
                .read(self.address, &mut buf)
                .map_err(Error::I2c)?;
            decode_measurement(&buf)
        }

        /// Activate the on-die heater for the given power & duration,
        /// then return the (high-precision) measurement taken at the end.
        pub fn measure_with_heater(
            &mut self,
            power: HeaterPower,
            duration: HeaterDuration,
        ) -> Result<Measurement, Error<I2C::Error>> {
            let cmd = types::heater_command(power, duration);
            self.i2c.write(self.address, &[cmd]).map_err(Error::I2c)?;
            self.delay.delay_us(duration.duration_us());
            let mut buf = [0u8; MEASUREMENT_LEN];
            self.i2c
                .read(self.address, &mut buf)
                .map_err(Error::I2c)?;
            decode_measurement(&buf)
        }

        /// Read the 32-bit factory serial number.
        pub fn read_serial_number(&mut self) -> Result<u32, Error<I2C::Error>> {
            self.i2c
                .write(self.address, &[commands::READ_SERIAL])
                .map_err(Error::I2c)?;
            self.delay.delay_us(commands::duration_us::SERIAL);
            let mut buf = [0u8; SERIAL_LEN];
            self.i2c
                .read(self.address, &mut buf)
                .map_err(Error::I2c)?;
            decode_serial(&buf)
        }

        /// Issue a soft reset. Waits ~1 ms for the sensor to recover.
        pub fn soft_reset(&mut self) -> Result<(), Error<I2C::Error>> {
            self.i2c
                .write(self.address, &[commands::SOFT_RESET])
                .map_err(Error::I2c)?;
            self.delay.delay_us(commands::duration_us::SOFT_RESET);
            Ok(())
        }
    }
}

#[cfg(feature = "async")]
#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
mod async_impl {
    use super::*;
    use embedded_hal_async::delay::DelayNs;
    use embedded_hal_async::i2c::I2c;

    impl<I2C, D> Sht4x<I2C, D>
    where
        I2C: I2c,
        D: DelayNs,
    {
        /// Async: trigger a measurement and read back temperature + humidity.
        pub async fn measure_async(
            &mut self,
            precision: Precision,
        ) -> Result<Measurement, Error<I2C::Error>> {
            self.i2c
                .write(self.address, &[precision.command()])
                .await
                .map_err(Error::I2c)?;
            self.delay.delay_us(precision.duration_us()).await;
            let mut buf = [0u8; MEASUREMENT_LEN];
            self.i2c
                .read(self.address, &mut buf)
                .await
                .map_err(Error::I2c)?;
            decode_measurement(&buf)
        }

        /// Async: heater-assisted measurement.
        pub async fn measure_with_heater_async(
            &mut self,
            power: HeaterPower,
            duration: HeaterDuration,
        ) -> Result<Measurement, Error<I2C::Error>> {
            let cmd = types::heater_command(power, duration);
            self.i2c
                .write(self.address, &[cmd])
                .await
                .map_err(Error::I2c)?;
            self.delay.delay_us(duration.duration_us()).await;
            let mut buf = [0u8; MEASUREMENT_LEN];
            self.i2c
                .read(self.address, &mut buf)
                .await
                .map_err(Error::I2c)?;
            decode_measurement(&buf)
        }

        /// Async: read the 32-bit factory serial number.
        pub async fn read_serial_number_async(&mut self) -> Result<u32, Error<I2C::Error>> {
            self.i2c
                .write(self.address, &[commands::READ_SERIAL])
                .await
                .map_err(Error::I2c)?;
            self.delay.delay_us(commands::duration_us::SERIAL).await;
            let mut buf = [0u8; SERIAL_LEN];
            self.i2c
                .read(self.address, &mut buf)
                .await
                .map_err(Error::I2c)?;
            decode_serial(&buf)
        }

        /// Async: issue a soft reset.
        pub async fn soft_reset_async(&mut self) -> Result<(), Error<I2C::Error>> {
            self.i2c
                .write(self.address, &[commands::SOFT_RESET])
                .await
                .map_err(Error::I2c)?;
            self.delay.delay_us(commands::duration_us::SOFT_RESET).await;
            Ok(())
        }
    }
}

#[doc(hidden)]
pub mod __private {
    //! Re-exports for the integration tests; not a stable API.
    pub use crate::crc::crc8;
}