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}