Skip to main content

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#[cfg(feature = "async")]
16use embedded_hal_async::delay::DelayNs as AsyncDelayNs;
17#[cfg(feature = "async")]
18use embedded_hal_async::i2c::I2c as AsyncI2c;
19
20/// Default I2C address of the HS3003 sensor
21pub const HS3003_I2C_ADDRESS: u8 = 0x44;
22
23/// Measurement settling time in microseconds
24const MEASUREMENT_TIME_US: u32 = 100_000; // 100ms
25
26/// HS3003 temperature and humidity sensor driver
27#[derive(Debug)]
28pub struct Hs3003<I2C> {
29    i2c: I2C,
30    address: u8,
31}
32
33/// Measurement result containing temperature and humidity
34#[derive(Debug, Clone, Copy, PartialEq)]
35pub struct Measurement {
36    /// Temperature in degrees Celsius
37    pub temperature: f32,
38    /// Relative humidity in percent
39    pub humidity: f32,
40}
41
42/// Errors that can occur when interacting with the sensor
43#[derive(Debug, Clone, Copy, PartialEq)]
44pub enum Error<E> {
45    /// I2C bus error
46    I2c(E),
47}
48
49impl<E> From<E> for Error<E> {
50    fn from(error: E) -> Self {
51        Error::I2c(error)
52    }
53}
54
55impl<I2C, E> Hs3003<I2C>
56where
57    I2C: I2c<Error = E>,
58{
59    /// Creates a new HS3003 driver instance with the default I2C address (0x44)
60    ///
61    /// # Arguments
62    ///
63    /// * `i2c` - An I2C interface implementing the `embedded_hal::i2c::I2c` trait
64    ///
65    /// # Example
66    ///
67    /// ```
68    /// # use embedded_hal_mock::eh1::i2c::{Mock as I2cMock, Transaction as I2cTransaction};
69    /// # use hs3003::Hs3003;
70    /// # let i2c = I2cMock::new(&[]);
71    /// let sensor = Hs3003::new(i2c);
72    /// # let mut i2c = sensor.destroy();
73    /// # i2c.done();
74    /// ```
75    pub fn new(i2c: I2C) -> Self {
76        Self::new_with_address(i2c, HS3003_I2C_ADDRESS)
77    }
78
79    /// Creates a new HS3003 driver instance with a custom I2C address
80    ///
81    /// # Arguments
82    ///
83    /// * `i2c` - An I2C interface implementing the `embedded_hal::i2c::I2c` trait
84    /// * `address` - Custom I2C address for the sensor
85    ///
86    /// # Example
87    ///
88    /// ```
89    /// # use embedded_hal_mock::eh1::i2c::{Mock as I2cMock, Transaction as I2cTransaction};
90    /// # use hs3003::Hs3003;
91    /// # let i2c = I2cMock::new(&[]);
92    /// let sensor = Hs3003::new_with_address(i2c, 0x44);
93    /// # let mut i2c = sensor.destroy();
94    /// # i2c.done();
95    /// ```
96    pub fn new_with_address(i2c: I2C, address: u8) -> Self {
97        Self { i2c, address }
98    }
99
100    /// Triggers a measurement and reads temperature and humidity
101    ///
102    /// This function:
103    /// 1. Sends a measurement request to the sensor
104    /// 2. Waits for the measurement to complete (100ms)
105    /// 3. Reads the raw data from the sensor
106    /// 4. Converts the raw data to temperature and humidity values
107    ///
108    /// # Arguments
109    ///
110    /// * `delay` - A delay provider implementing `embedded_hal::delay::DelayNs`
111    ///
112    /// # Returns
113    ///
114    /// A `Result` containing a `Measurement` with temperature and humidity values,
115    /// or an `Error` if the operation fails.
116    ///
117    /// # Example
118    ///
119    /// ```
120    /// # use embedded_hal_mock::eh1::i2c::{Mock as I2cMock, Transaction as I2cTransaction};
121    /// # use embedded_hal_mock::eh1::delay::NoopDelay;
122    /// # use hs3003::Hs3003;
123    /// # let expectations = [
124    /// #     I2cTransaction::write(0x44, vec![0x00]),
125    /// #     I2cTransaction::read(0x44, vec![0x1F, 0xFF, 0x66, 0x64]),
126    /// # ];
127    /// # let i2c = I2cMock::new(&expectations);
128    /// # let mut delay = NoopDelay::new();
129    /// let mut sensor = Hs3003::new(i2c);
130    /// let measurement = sensor.read(&mut delay)?;
131    /// // Use measurement.temperature and measurement.humidity
132    /// # let mut i2c = sensor.destroy();
133    /// # i2c.done();
134    /// # Ok::<(), hs3003::Error<embedded_hal::i2c::ErrorKind>>(())
135    /// ```
136    pub fn read<D>(&mut self, delay: &mut D) -> Result<Measurement, Error<E>>
137    where
138        D: DelayNs,
139    {
140        // Trigger measurement by writing to the sensor
141        self.i2c.write(self.address, &[0x00])?;
142
143        // Wait for measurement to complete
144        delay.delay_us(MEASUREMENT_TIME_US);
145
146        // Read 4 bytes of data
147        let mut buffer = [0u8; 4];
148        self.i2c.read(self.address, &mut buffer)?;
149
150        // Parse the measurement
151        Ok(Self::parse_measurement(&buffer))
152    }
153
154    /// Destroys the driver and returns the I2C interface
155    ///
156    /// This allows the I2C bus to be reused for other devices.
157    ///
158    /// # Example
159    ///
160    /// ```
161    /// # use embedded_hal_mock::eh1::i2c::{Mock as I2cMock, Transaction as I2cTransaction};
162    /// # use hs3003::Hs3003;
163    /// # let i2c = I2cMock::new(&[]);
164    /// let sensor = Hs3003::new(i2c);
165    /// let mut i2c = sensor.destroy();
166    /// # i2c.done();
167    /// ```
168    pub fn destroy(self) -> I2C {
169        self.i2c
170    }
171}
172
173#[cfg(feature = "async")]
174impl<I2C, E> Hs3003<I2C>
175where
176    I2C: AsyncI2c<Error = E>,
177{
178    /// Triggers a measurement and reads temperature and humidity asynchronously
179    ///
180    /// This function:
181    /// 1. Sends a measurement request to the sensor
182    /// 2. Waits for the measurement to complete (100ms)
183    /// 3. Reads the raw data from the sensor
184    /// 4. Converts the raw data to temperature and humidity values
185    ///
186    /// # Arguments
187    ///
188    /// * `delay` - A delay provider implementing `embedded_hal_async::delay::DelayNs`
189    ///
190    /// # Returns
191    ///
192    /// A `Result` containing a `Measurement` with temperature and humidity values,
193    /// or an `Error` if the operation fails.
194    pub async fn read_async<D>(&mut self, delay: &mut D) -> Result<Measurement, Error<E>>
195    where
196        D: AsyncDelayNs,
197    {
198        // Trigger measurement by writing to the sensor
199        self.i2c.write(self.address, &[0x00]).await?;
200
201        // Wait for measurement to complete
202        delay.delay_us(MEASUREMENT_TIME_US).await;
203
204        // Read 4 bytes of data
205        let mut buffer = [0u8; 4];
206        self.i2c.read(self.address, &mut buffer).await?;
207
208        // Parse the measurement
209        Ok(Self::parse_measurement(&buffer))
210    }
211}
212
213// Separate impl block without trait bounds for parsing (allows testing)
214impl<I2C> Hs3003<I2C> {
215    /// Parses raw sensor data into a Measurement
216    ///
217    /// The HS3003 returns 4 bytes:
218    /// - Bytes 0-1: Humidity data (14 bits, upper 2 bits are status)
219    /// - Bytes 2-3: Temperature data (14 bits, lower 2 bits are unused)
220    ///
221    /// Humidity calculation: (raw_value / 16383) * 100
222    /// Temperature calculation: ((raw_value / 16383) * 165) - 40
223    fn parse_measurement(data: &[u8; 4]) -> Measurement {
224        // Extract humidity from first two bytes (top 14 bits)
225        let humidity_raw = u16::from_be_bytes([data[0] & 0x3F, data[1]]);
226        let humidity = (f32::from(humidity_raw) / 16383.0) * 100.0;
227
228        // Extract temperature from last two bytes (shift right 2 bits for 14-bit value)
229        let temp_raw = u16::from_be_bytes([data[2], data[3]]) >> 2;
230        let temperature = ((f32::from(temp_raw) / 16383.0) * 165.0) - 40.0;
231
232        Measurement {
233            temperature,
234            humidity,
235        }
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    extern crate std;
242    use super::*;
243
244    #[test]
245    fn test_parse_measurement_typical() {
246        // Test with typical values
247        // Humidity: 50% RH -> raw value = 8191 (0x1FFF)
248        // Temperature: 25°C -> raw value = 6553 (0x1999)
249        let data = [
250            0x1F, 0xFF, // Humidity: 50% (with status bits clear)
251            0x66, 0x64, // Temperature: 25°C (shifted left 2 bits)
252        ];
253
254        let measurement = Hs3003::<()>::parse_measurement(&data);
255
256        // Temperature: (6553 / 16383.0) * 165.0 - 40.0 = 26.0°C
257        // Allow small floating point error
258        assert!((measurement.humidity - 50.0).abs() < 0.1);
259        assert!(
260            (measurement.temperature - 26.0).abs() < 0.5,
261            "Temperature was {}",
262            measurement.temperature
263        );
264    }
265
266    #[test]
267    fn test_parse_measurement_min_max() {
268        // Test minimum values (0% RH, -40°C)
269        let data_min = [0x00, 0x00, 0x00, 0x00];
270        let measurement_min = Hs3003::<()>::parse_measurement(&data_min);
271        assert!((measurement_min.humidity - 0.0).abs() < 0.1);
272        assert!((measurement_min.temperature - (-40.0)).abs() < 0.5);
273
274        // Test maximum values (100% RH, 125°C)
275        let data_max = [0xFF, 0xFF, 0xFF, 0xFC];
276        let measurement_max = Hs3003::<()>::parse_measurement(&data_max);
277        assert!((measurement_max.humidity - 100.0).abs() < 0.1);
278        assert!((measurement_max.temperature - 125.0).abs() < 0.5);
279    }
280
281    #[test]
282    fn test_default_address() {
283        assert_eq!(HS3003_I2C_ADDRESS, 0x44);
284    }
285
286    #[test]
287    fn test_read_sync() {
288        use embedded_hal_mock::eh1::delay::NoopDelay;
289        use embedded_hal_mock::eh1::i2c::{Mock as I2cMock, Transaction as I2cTransaction};
290
291        let expectations = [
292            I2cTransaction::write(HS3003_I2C_ADDRESS, std::vec![0x00]),
293            I2cTransaction::read(HS3003_I2C_ADDRESS, std::vec![0x1F, 0xFF, 0x66, 0x64]),
294        ];
295        let i2c = I2cMock::new(&expectations);
296        let mut delay = NoopDelay::new();
297        let mut sensor = Hs3003::new(i2c);
298
299        let measurement = sensor.read(&mut delay).unwrap();
300        assert!((measurement.humidity - 50.0).abs() < 0.1);
301        assert!((measurement.temperature - 26.0).abs() < 0.5);
302
303        let mut i2c = sensor.destroy();
304        i2c.done();
305    }
306
307    #[cfg(feature = "async")]
308    #[test]
309    fn test_read_async() {
310        use embedded_hal_mock::eh1::delay::NoopDelay;
311        use embedded_hal_mock::eh1::i2c::{Mock as I2cMock, Transaction as I2cTransaction};
312        use futures::executor::block_on;
313
314        let expectations = [
315            I2cTransaction::write(HS3003_I2C_ADDRESS, std::vec![0x00]),
316            I2cTransaction::read(HS3003_I2C_ADDRESS, std::vec![0x1F, 0xFF, 0x66, 0x64]),
317        ];
318        let i2c = I2cMock::new(&expectations);
319        let mut delay = NoopDelay::new();
320        let mut sensor = Hs3003::new(i2c);
321
322        let measurement = block_on(sensor.read_async(&mut delay)).unwrap();
323        assert!((measurement.humidity - 50.0).abs() < 0.1);
324        assert!((measurement.temperature - 26.0).abs() < 0.5);
325
326        let mut i2c = sensor.destroy();
327        i2c.done();
328    }
329}