hs3003/
lib.rs

1#![no_std]
2#![doc = include_str!("../README.md")]
3#![deny(missing_docs)]
4#![deny(warnings)]
5
6//! Platform-agnostic Rust driver for the Renesas HS3003 temperature and humidity sensor.
7//!
8//! This driver uses the `embedded-hal` traits to provide a hardware-independent interface
9//! to the HS3003 sensor. It supports reading both temperature and humidity measurements
10//! over I2C.
11
12use embedded_hal::delay::DelayNs;
13use embedded_hal::i2c::I2c;
14
15/// Default I2C address of the HS3003 sensor
16pub const HS3003_I2C_ADDRESS: u8 = 0x44;
17
18/// Measurement settling time in microseconds
19const MEASUREMENT_TIME_US: u32 = 100_000; // 100ms
20
21/// HS3003 temperature and humidity sensor driver
22#[derive(Debug)]
23pub struct Hs3003<I2C> {
24    i2c: I2C,
25    address: u8,
26}
27
28/// Measurement result containing temperature and humidity
29#[derive(Debug, Clone, Copy, PartialEq)]
30pub struct Measurement {
31    /// Temperature in degrees Celsius
32    pub temperature: f32,
33    /// Relative humidity in percent
34    pub humidity: f32,
35}
36
37/// Errors that can occur when interacting with the sensor
38#[derive(Debug, Clone, Copy, PartialEq)]
39pub enum Error<E> {
40    /// I2C bus error
41    I2c(E),
42}
43
44impl<E> From<E> for Error<E> {
45    fn from(error: E) -> Self {
46        Error::I2c(error)
47    }
48}
49
50impl<I2C, E> Hs3003<I2C>
51where
52    I2C: I2c<Error = E>,
53{
54    /// Creates a new HS3003 driver instance with the default I2C address (0x44)
55    ///
56    /// # Arguments
57    ///
58    /// * `i2c` - An I2C interface implementing the `embedded_hal::i2c::I2c` trait
59    ///
60    /// # Example
61    ///
62    /// ```
63    /// # use embedded_hal_mock::eh1::i2c::{Mock as I2cMock, Transaction as I2cTransaction};
64    /// # use hs3003::Hs3003;
65    /// # let i2c = I2cMock::new(&[]);
66    /// let sensor = Hs3003::new(i2c);
67    /// # let mut i2c = sensor.destroy();
68    /// # i2c.done();
69    /// ```
70    pub fn new(i2c: I2C) -> Self {
71        Self::new_with_address(i2c, HS3003_I2C_ADDRESS)
72    }
73
74    /// Creates a new HS3003 driver instance with a custom I2C address
75    ///
76    /// # Arguments
77    ///
78    /// * `i2c` - An I2C interface implementing the `embedded_hal::i2c::I2c` trait
79    /// * `address` - Custom I2C address for the sensor
80    ///
81    /// # Example
82    ///
83    /// ```
84    /// # use embedded_hal_mock::eh1::i2c::{Mock as I2cMock, Transaction as I2cTransaction};
85    /// # use hs3003::Hs3003;
86    /// # let i2c = I2cMock::new(&[]);
87    /// let sensor = Hs3003::new_with_address(i2c, 0x44);
88    /// # let mut i2c = sensor.destroy();
89    /// # i2c.done();
90    /// ```
91    pub fn new_with_address(i2c: I2C, address: u8) -> Self {
92        Self { i2c, address }
93    }
94
95    /// Triggers a measurement and reads temperature and humidity
96    ///
97    /// This function:
98    /// 1. Sends a measurement request to the sensor
99    /// 2. Waits for the measurement to complete (100ms)
100    /// 3. Reads the raw data from the sensor
101    /// 4. Converts the raw data to temperature and humidity values
102    ///
103    /// # Arguments
104    ///
105    /// * `delay` - A delay provider implementing `embedded_hal::delay::DelayNs`
106    ///
107    /// # Returns
108    ///
109    /// A `Result` containing a `Measurement` with temperature and humidity values,
110    /// or an `Error` if the operation fails.
111    ///
112    /// # Example
113    ///
114    /// ```
115    /// # use embedded_hal_mock::eh1::i2c::{Mock as I2cMock, Transaction as I2cTransaction};
116    /// # use embedded_hal_mock::eh1::delay::NoopDelay;
117    /// # use hs3003::Hs3003;
118    /// # let expectations = [
119    /// #     I2cTransaction::write(0x44, vec![0x00]),
120    /// #     I2cTransaction::read(0x44, vec![0x1F, 0xFF, 0x66, 0x64]),
121    /// # ];
122    /// # let i2c = I2cMock::new(&expectations);
123    /// # let mut delay = NoopDelay::new();
124    /// let mut sensor = Hs3003::new(i2c);
125    /// let measurement = sensor.read(&mut delay)?;
126    /// // Use measurement.temperature and measurement.humidity
127    /// # let mut i2c = sensor.destroy();
128    /// # i2c.done();
129    /// # Ok::<(), hs3003::Error<embedded_hal::i2c::ErrorKind>>(())
130    /// ```
131    pub fn read<D>(&mut self, delay: &mut D) -> Result<Measurement, Error<E>>
132    where
133        D: DelayNs,
134    {
135        // Trigger measurement by writing to the sensor
136        self.i2c.write(self.address, &[0x00])?;
137
138        // Wait for measurement to complete
139        delay.delay_us(MEASUREMENT_TIME_US);
140
141        // Read 4 bytes of data
142        let mut buffer = [0u8; 4];
143        self.i2c.read(self.address, &mut buffer)?;
144
145        // Parse the measurement
146        Ok(Self::parse_measurement(&buffer))
147    }
148
149    /// Destroys the driver and returns the I2C interface
150    ///
151    /// This allows the I2C bus to be reused for other devices.
152    ///
153    /// # Example
154    ///
155    /// ```
156    /// # use embedded_hal_mock::eh1::i2c::{Mock as I2cMock, Transaction as I2cTransaction};
157    /// # use hs3003::Hs3003;
158    /// # let i2c = I2cMock::new(&[]);
159    /// let sensor = Hs3003::new(i2c);
160    /// let mut i2c = sensor.destroy();
161    /// # i2c.done();
162    /// ```
163    pub fn destroy(self) -> I2C {
164        self.i2c
165    }
166}
167
168// Separate impl block without trait bounds for parsing (allows testing)
169impl<I2C> Hs3003<I2C> {
170    /// Parses raw sensor data into a Measurement
171    ///
172    /// The HS3003 returns 4 bytes:
173    /// - Bytes 0-1: Humidity data (14 bits, upper 2 bits are status)
174    /// - Bytes 2-3: Temperature data (14 bits, lower 2 bits are unused)
175    ///
176    /// Humidity calculation: (raw_value / 16383) * 100
177    /// Temperature calculation: ((raw_value / 16383) * 165) - 40
178    fn parse_measurement(data: &[u8; 4]) -> Measurement {
179        // Extract humidity from first two bytes (top 14 bits)
180        let humidity_raw = u16::from_be_bytes([data[0] & 0x3F, data[1]]);
181        let humidity = (f32::from(humidity_raw) / 16383.0) * 100.0;
182
183        // Extract temperature from last two bytes (shift right 2 bits for 14-bit value)
184        let temp_raw = u16::from_be_bytes([data[2], data[3]]) >> 2;
185        let temperature = ((f32::from(temp_raw) / 16383.0) * 165.0) - 40.0;
186
187        Measurement {
188            temperature,
189            humidity,
190        }
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn test_parse_measurement_typical() {
200        // Test with typical values
201        // Humidity: 50% RH -> raw value = 8191 (0x1FFF)
202        // Temperature: 25°C -> raw value = 6553 (0x1999)
203        let data = [
204            0x1F, 0xFF, // Humidity: 50% (with status bits clear)
205            0x66, 0x64, // Temperature: 25°C (shifted left 2 bits)
206        ];
207
208        let measurement = Hs3003::<()>::parse_measurement(&data);
209
210        // Temperature: (6553 / 16383.0) * 165.0 - 40.0 = 26.0°C
211        // Allow small floating point error
212        assert!((measurement.humidity - 50.0).abs() < 0.1);
213        assert!(
214            (measurement.temperature - 26.0).abs() < 0.5,
215            "Temperature was {}",
216            measurement.temperature
217        );
218    }
219
220    #[test]
221    fn test_parse_measurement_min_max() {
222        // Test minimum values (0% RH, -40°C)
223        let data_min = [0x00, 0x00, 0x00, 0x00];
224        let measurement_min = Hs3003::<()>::parse_measurement(&data_min);
225        assert!((measurement_min.humidity - 0.0).abs() < 0.1);
226        assert!((measurement_min.temperature - (-40.0)).abs() < 0.5);
227
228        // Test maximum values (100% RH, 125°C)
229        let data_max = [0xFF, 0xFF, 0xFF, 0xFC];
230        let measurement_max = Hs3003::<()>::parse_measurement(&data_max);
231        assert!((measurement_max.humidity - 100.0).abs() < 0.1);
232        assert!((measurement_max.temperature - 125.0).abs() < 0.5);
233    }
234
235    #[test]
236    fn test_default_address() {
237        assert_eq!(HS3003_I2C_ADDRESS, 0x44);
238    }
239}