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}