gy-21 0.2.0

Driver for the GY-21 temperature and relative humidity sensor
Documentation
#![no_std]

//! Crate to interface with the [HTU21D](https://www.te.com/commerce/DocumentDelivery/DDEController?Action=showdoc&DocId=Data+Sheet%7FHPC199_6%7FA6%7Fpdf%7FEnglish%7FENG_DS_HPC199_6_A6.pdf%7FCAT-HSC0004) temperature and relaive humidity sensor (commonly referred to as GY-21)
//!
//! Implements `Read` and `Write` from [`embedded_hal::blocking::i2c`](https://docs.rs/embedded-hal/latest/embedded_hal/blocking/i2c/index.html)
//!
//! This crate does not implement all the features of the HTU21D. It doesn't implement the master
//! hold read or the precision change. It also ignores the status bits since the sensor I used to
//! develop this doesn't seem to use those in the same way as the datasheet does
//!
//! # How to use
//! Create the GY-21:
//! ```no_run
//! // `delay` is your HAL's delay object
//! // `i2c` is a i2c bus you have previously prepared
//!
//! let gy_21 = Gy21::new(i2c, delay); // Using the default I2C address for the sensor (0x40)
//! // otherwise
//! let gy_21 = Gy21::with_address(i2c, delay, 0x40) // Using 0x39 as the I2C address
//! ```
//!
//! Read the temperature, humidity and dew point temperature:
//! ```no_run
//! if let Ok(temp) = gy_21.temperature() {
//!     println!("Temperature = {}", temp);
//! }
//! if let Ok(rhum) = gy_21.humidity() {
//!     println!("Relative humidity = {}%", rhum);
//! }
//! if let Ok(dpt) = gy_21.dew_point_temp() {
//!     println!("Dew point temperature = {}\n\n", dpt);
//! }
//! ```

use embedded_hal::blocking::{
    delay::DelayMs,
    i2c::{Read, Write},
};

/// Represents the GY-21 sensor
pub struct Gy21<I2C, DL> {
    i2c: I2C,
    address: u8,
    delay: DL,
}

/// Error type containing I2C Read and Write errors
pub enum CommError<I2C: Write + Read> {
    /// I2C Write error
    Write(<I2C as Write>::Error),

    /// I2C Read error
    Read(<I2C as Read>::Error),
}

/// Converts from I2C errors to `CommErrors` without using `From` becayse that conflicts with
/// `core::convert`
impl<I2C: Write + Read> CommError<I2C> {
    pub const fn from_write_error(err: <I2C as Write>::Error) -> Self {
        Self::Write(err)
    }

    pub const fn from_read_error(err: <I2C as Read>::Error) -> Self {
        Self::Read(err)
    }
}

impl<I2C: Write + Read, DL> Gy21<I2C, DL>
where
    DL: DelayMs<u8>,
{
    const DEFAULT_I2C_ADDRESS: u8 = 0x40;
    const READ_TEMP: u8 = 0xF3; // Trigger a temp measurement without the sensor holding the clock
    const READ_RHUM: u8 = 0xF5; // Trigger a rhum measurement without the sensor holding the clock

    const TWO_POW16: f32 = 65536.0;
    const LAST_2BIT: u8 = 0b0000_0011;

    /// Create a new GY-21 sensor from an I2C bus and a delay object with the default I2C address
    /// (`0x40`)
    ///
    /// The I2C object must be created with a frequency <= 400kHz, which is the maximum the sensor
    /// is able to keep up with as stated by the datasheet.
    ///
    /// ```no_run
    /// let gy_21 = Gy21::new(i2c, delay);
    /// ```
    pub const fn new(i2c: I2C, delay: DL) -> Self {
        Self {
            i2c,
            address: Self::DEFAULT_I2C_ADDRESS,
            delay,
        }
    }

    /// Create a new GY-21 sensor from an I2C bus, a delay object and an I2C address
    ///
    /// ```no_run
    /// let gy_21 = Gy21::with_address(i2c, delay, 0x40);
    /// ```
    pub const fn with_address(i2c: I2C, delay: DL, address: u8) -> Self {
        Self {
            i2c,
            address,
            delay,
        }
    }

    fn read_sensor(&mut self, word: u8) -> Result<u16, CommError<I2C>> {
        // A reading is requested by sending the word Self::READ_TEMP at self.address
        // The sensor won't reply while it's reading so the function must wait before reading I2C.
        // Sensor replies with 16 bits. The phrase has the following structure:
        // [ [m m m m m m m m] [l l l l l l s s] ]
        // Where:
        //  - m is a bit in the MSB part of the reading
        //  - l is a bit in the LSB part of the reading
        //  - s is a status bit

        self.i2c
            .write(self.address, &[word])
            .map_err(CommError::from_write_error)?;

        self.delay.delay_ms(50); // Wait for the sensor to read

        let mut buf = [0, 0];
        self.i2c
            .read(self.address, &mut buf)
            .map_err(CommError::from_read_error)?;

        let msb = (buf[0] as u16) << 8; // Set the first 8 bits of the message as the MSB of a u16
        let lsb = (buf[1] & !Self::LAST_2BIT) as u16; // Discard the status bits and cast to u16
        Ok(msb | lsb)
    }

    /// Read the temperature
    ///
    /// ```no_run
    /// let reading = match gy_21.temperature() {
    ///     Ok(val) => val;
    ///     Err(_) => panic()!
    /// }
    /// ```
    ///
    /// # Errors
    /// Will return an error in the following cases:
    ///  - `Write(i2c_err)` if writing to the I2C bus fails
    ///  - `Read(i2c_err)` if reading the I2C bus fails
    pub fn temperature(&mut self) -> Result<f32, CommError<I2C>> {
        let reading = self.read_sensor(Self::READ_TEMP)?;
        // Temperature is calculated with the forula provided by the datasheet
        // T = -46.85 + 175.72 * (signal_output / 2^16)
        let temperature = -46.85 + 175.72 * (reading as f32 / Self::TWO_POW16);
        Ok(temperature)
    }

    /// Read the relative humidity
    ///
    /// ```no_run
    /// let reading = match gy_21.humidity() {
    ///     Ok(val) => val;
    ///     Err(_) => panic()!
    /// }
    /// ```
    ///
    /// # Errors
    /// Will return an error in the following cases:
    ///  - `Write(i2c_err)` if writing to the I2C bus fails
    ///  - `Read(i2c_err)` if reading the I2C bus fails
    pub fn humidity(&mut self) -> Result<f32, CommError<I2C>> {
        let reading = self.read_sensor(Self::READ_RHUM)?;
        // Relative humidity is calculated with the forula provided by the datasheet
        // rH=-6+125 * (signal_output / 2^16)
        let rhumidty = -6.0 + 125.0 * (reading as f32 / Self::TWO_POW16);
        Ok(rhumidty)
    }

    /// Calculate the dew point temperature based on temperature and relative humidity readings
    ///
    /// The dew point of a given body of air is the temperature to which it must be cooled to become saturated with water vapor.
    ///
    /// ```no_run
    /// let reading = match gy_21.dew_point_temp() {
    ///     Ok(val) => val;
    ///     Err(_) => panic()!
    /// }
    /// ```
    ///
    /// # Errors
    /// Will return an error in the following cases:
    ///  - `Write(i2c_err)` if writing to the I2C bus fails
    ///  - `Read(i2c_err)` if reading the I2C bus fails
    pub fn dew_point_temp(&mut self) -> Result<f32, CommError<I2C>> {
        use libm::Libm;
        let temp = self.temperature()?;
        let rhum = self.humidity()?;

        // Constants and formulas as defined by the datasheet
        let (a, b, c) = (8.1332, 1762.39, 235.66);
        let part_pressure = Libm::<f32>::exp10(a - (b / (temp + c)));
        let dew_point_temp = -((b / (Libm::<f32>::log10(rhum * (part_pressure / 100.0)) - a)) + c);
        Ok(dew_point_temp)
    }
}