Skip to main content

pamoja_sensors/sht3x/
driver.rs

1//! The SHT3x driven over I2C: reset, confirm the part answers, and measure on demand.
2
3use embedded_hal::delay::DelayNs;
4use embedded_hal::i2c::I2c;
5use pamoja_core::Sensor;
6
7use super::{
8    command, single_shot, Measurement, Repeatability, Status, MIN_COMMAND_GAP_MICROS,
9    SOFT_RESET_MICROS,
10};
11use crate::error::DriverError;
12
13/// An SHT3x on an I2C bus, measuring on demand in single-shot mode.
14///
15/// [`Sht3x::new`] wraps the bus. [`init`](Sht3x::init) soft-resets the part, waits the
16/// datasheet's reset time, and reads the status register: the part has no id
17/// register, so a status word whose CRC checks is what confirms an SHT3x answers at
18/// the address. [`measure`](Sht3x::measure) issues one single-shot command without
19/// clock stretching, waits the datasheet's maximum measurement duration for the
20/// repeatability in use, reads the six data bytes, and returns the CRC-checked
21/// [`Measurement`]. Every command is followed by the wait the datasheet attaches to
22/// it (the reset time, the measurement duration, or the 1 ms between commands), so
23/// consecutive calls keep the spacing the part needs. As a [`Sensor`] its reading is
24/// the whole measurement; one channel is selected with [`Sensor::map`].
25///
26/// # Examples
27///
28/// A proofing cabinet's climate, read from an SHT3x that is not plugged in yet: the
29/// simulated part, which the `alloc` feature brings, holds 27 °C and 75 % relative
30/// humidity.
31///
32/// ```
33/// # #[cfg(feature = "alloc")]
34/// # {
35/// use pamoja_core::Sensor;
36/// use pamoja_hal::script::{block_on, DelayLog};
37/// use pamoja_sensors::sht3x::{sim, Sht3x, I2C_ADDRESS_A};
38///
39/// let part = sim::reporting(I2C_ADDRESS_A, 27.0, 75.0);
40/// let mut sensor = Sht3x::new(part, I2C_ADDRESS_A, DelayLog::new());
41/// let climate = block_on(sensor.read())?;
42/// assert!((climate.temperature_milli_celsius() - 27_000).abs() < 10);
43/// assert!(climate.humidity_milli_percent().abs_diff(75_000) < 10);
44/// # }
45/// # Ok::<(), pamoja_core::Error>(())
46/// ```
47#[derive(Debug)]
48pub struct Sht3x<I2C, D> {
49    bus: I2C,
50    address: u8,
51    delay: D,
52    repeatability: Repeatability,
53    status: Option<Status>,
54}
55
56impl<I2C, D> Sht3x<I2C, D> {
57    /// Wraps an I2C bus and the part's address.
58    ///
59    /// # Arguments
60    ///
61    /// * `bus` - the I2C bus the part is on.
62    /// * `address` - [`I2C_ADDRESS_A`](super::I2C_ADDRESS_A) with ADDR low,
63    ///   [`I2C_ADDRESS_B`](super::I2C_ADDRESS_B) with ADDR high.
64    /// * `delay` - the timer that paces the reset, the measurements, and the gaps
65    ///   between commands.
66    ///
67    /// # Returns
68    ///
69    /// The driver, measuring at high repeatability.
70    pub fn new(bus: I2C, address: u8, delay: D) -> Self {
71        Sht3x {
72            bus,
73            address,
74            delay,
75            repeatability: Repeatability::High,
76            status: None,
77        }
78    }
79
80    /// Sets the repeatability of each measurement, which trades noise against
81    /// measurement time and energy.
82    ///
83    /// # Arguments
84    ///
85    /// * `repeatability` - the repeatability to measure at.
86    ///
87    /// # Returns
88    ///
89    /// The driver.
90    pub fn with_repeatability(mut self, repeatability: Repeatability) -> Self {
91        self.repeatability = repeatability;
92        self
93    }
94
95    /// Returns the part's 7-bit address.
96    pub fn address(&self) -> u8 {
97        self.address
98    }
99
100    /// Returns the status register as it was last read, if it has been.
101    pub fn status(&self) -> Option<Status> {
102        self.status
103    }
104
105    /// Gives back the bus and the delay.
106    ///
107    /// # Returns
108    ///
109    /// The bus and delay the driver was built from.
110    pub fn release(self) -> (I2C, D) {
111        (self.bus, self.delay)
112    }
113}
114
115impl<I2C: I2c, D: DelayNs> Sht3x<I2C, D> {
116    /// Resets the part and reads its status register.
117    ///
118    /// The sequence is the datasheet's: the soft-reset command of Table 14, the
119    /// soft-reset time of Table 4, then the status read of Table 17. The part has no
120    /// id register, so a status word whose CRC checks is the confirmation that an
121    /// SHT3x answers at the address. The reset also switches the heater off.
122    ///
123    /// # Errors
124    ///
125    /// Returns [`DriverError::Bus`] if the bus fails and [`DriverError::Sensor`] with
126    /// [`SensorError::Crc`](crate::SensorError::Crc) if the status word does not
127    /// check, so whatever answered is not a working SHT3x.
128    pub fn init(&mut self) -> Result<(), DriverError<I2C::Error>> {
129        self.send(command::SOFT_RESET)?;
130        self.delay.delay_us(SOFT_RESET_MICROS);
131        self.read_status()?;
132        Ok(())
133    }
134
135    /// Runs one single-shot measurement and returns the CRC-checked result.
136    ///
137    /// Initializes the part first if [`init`](Sht3x::init) has not run. The sequence
138    /// is sections 4.2 to 4.4 of the datasheet: the Table 9 command for the
139    /// repeatability in use with clock stretching disabled, a wait of Table 4's
140    /// maximum measurement duration, then one read of the temperature word, its CRC,
141    /// the humidity word, and its CRC. Table 4 holds from 2.4 V up; below that
142    /// (Table 5) the part can take 0.5 ms longer and answers a read that comes early
143    /// with a NACK, which the bus reports as its error.
144    ///
145    /// # Returns
146    ///
147    /// The measurement.
148    ///
149    /// # Errors
150    ///
151    /// Returns [`DriverError::Bus`] if the bus fails, [`DriverError::Sensor`] with
152    /// [`SensorError::Crc`](crate::SensorError::Crc) if either data word does not
153    /// check, or any error of [`init`](Sht3x::init).
154    pub fn measure(&mut self) -> Result<Measurement, DriverError<I2C::Error>> {
155        self.initialize_once()?;
156        self.send(single_shot(self.repeatability, false))?;
157        let wait = self.repeatability.max_measurement_micros();
158        self.delay.delay_us(wait);
159        let mut bytes = [0u8; 6];
160        self.bus
161            .read(self.address, &mut bytes)
162            .map_err(DriverError::Bus)?;
163        Measurement::parse(&bytes).map_err(DriverError::Sensor)
164    }
165
166    /// Reads the status register.
167    ///
168    /// The command of Table 17, the three bytes that answer it (the register most
169    /// significant byte first, then its CRC), and the 1 ms the part needs before its
170    /// next command.
171    ///
172    /// # Returns
173    ///
174    /// The decoded status, which [`status`](Sht3x::status) keeps as well.
175    ///
176    /// # Errors
177    ///
178    /// Returns [`DriverError::Bus`] if the bus fails and [`DriverError::Sensor`] with
179    /// [`SensorError::Crc`](crate::SensorError::Crc) if the word does not check.
180    pub fn read_status(&mut self) -> Result<Status, DriverError<I2C::Error>> {
181        self.send(command::READ_STATUS)?;
182        let mut bytes = [0u8; 3];
183        self.bus
184            .read(self.address, &mut bytes)
185            .map_err(DriverError::Bus)?;
186        self.delay.delay_us(MIN_COMMAND_GAP_MICROS);
187        let status = Status::parse(&bytes).map_err(DriverError::Sensor)?;
188        self.status = Some(status);
189        Ok(status)
190    }
191
192    /// Switches the plausibility-check heater on.
193    ///
194    /// Initializes the part first if [`init`](Sht3x::init) has not run, since the
195    /// reset in it would switch the heater off again. Sends the enable command of
196    /// Table 16 and waits the 1 ms the part needs before its next command. The
197    /// heater's state shows in [`Status::heater_on`] on the next
198    /// [`read_status`](Sht3x::read_status).
199    ///
200    /// # Errors
201    ///
202    /// Returns [`DriverError::Bus`] if the bus fails, or any error of
203    /// [`init`](Sht3x::init).
204    pub fn heater_on(&mut self) -> Result<(), DriverError<I2C::Error>> {
205        self.initialize_once()?;
206        self.send(command::HEATER_ENABLE)?;
207        self.delay.delay_us(MIN_COMMAND_GAP_MICROS);
208        Ok(())
209    }
210
211    /// Switches the heater off, which is its state after any reset.
212    ///
213    /// Initializes the part first if [`init`](Sht3x::init) has not run, then sends
214    /// the disable command of Table 16 and waits the 1 ms the part needs before its
215    /// next command.
216    ///
217    /// # Errors
218    ///
219    /// Returns [`DriverError::Bus`] if the bus fails, or any error of
220    /// [`init`](Sht3x::init).
221    pub fn heater_off(&mut self) -> Result<(), DriverError<I2C::Error>> {
222        self.initialize_once()?;
223        self.send(command::HEATER_DISABLE)?;
224        self.delay.delay_us(MIN_COMMAND_GAP_MICROS);
225        Ok(())
226    }
227
228    fn initialize_once(&mut self) -> Result<(), DriverError<I2C::Error>> {
229        if self.status.is_none() {
230            self.init()?;
231        }
232        Ok(())
233    }
234
235    fn send(&mut self, word: u16) -> Result<(), DriverError<I2C::Error>> {
236        self.bus
237            .write(self.address, &word.to_be_bytes())
238            .map_err(DriverError::Bus)
239    }
240}
241
242impl<I2C, D> Sensor for Sht3x<I2C, D>
243where
244    I2C: I2c + Send,
245    I2C::Error: core::fmt::Debug,
246    D: DelayNs + Send,
247{
248    type Reading = Measurement;
249
250    async fn read(&mut self) -> pamoja_core::Result<Measurement> {
251        self.measure().map_err(pamoja_core::Error::from)
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258    use crate::sht3x::{
259        humidity_raw_from_relative_humidity, temperature_raw_from_celsius, I2C_ADDRESS_A,
260        I2C_ADDRESS_B,
261    };
262    use crate::SensorError;
263    use alloc::vec;
264    use alloc::vec::Vec;
265    use embedded_hal::i2c::{ErrorKind, NoAcknowledgeSource};
266    use pamoja_hal::script::{block_on, DelayLog, I2cScript, I2cStep};
267
268    const SOFT_RESET: [u8; 2] = [0x30, 0xA2];
269    const READ_STATUS: [u8; 2] = [0xF3, 0x2D];
270    const SINGLE_SHOT_HIGH: [u8; 2] = [0x24, 0x00];
271    const HEATER_ENABLE: [u8; 2] = [0x30, 0x6D];
272    const HEATER_DISABLE: [u8; 2] = [0x30, 0x66];
273
274    fn part_at(celsius: f32, percent: f32) -> Measurement {
275        Measurement {
276            temperature_raw: temperature_raw_from_celsius(celsius),
277            humidity_raw: humidity_raw_from_relative_humidity(percent),
278        }
279    }
280
281    fn init_steps(address: u8) -> Vec<I2cStep> {
282        vec![
283            I2cStep::write(address, SOFT_RESET),
284            I2cStep::write(address, READ_STATUS),
285            I2cStep::read(address, Status::from_bits(Status::DEFAULT).to_bytes()),
286        ]
287    }
288
289    fn measure_steps(address: u8, command: [u8; 2], reply: [u8; 6]) -> Vec<I2cStep> {
290        vec![
291            I2cStep::write(address, command),
292            I2cStep::read(address, reply),
293        ]
294    }
295
296    #[test]
297    fn init_resets_and_confirms_the_part_and_measure_follows_the_single_shot_sequence() {
298        let part = part_at(25.0, 60.0);
299        let mut steps = init_steps(I2C_ADDRESS_A);
300        steps.extend(measure_steps(
301            I2C_ADDRESS_A,
302            SINGLE_SHOT_HIGH,
303            part.to_bytes(),
304        ));
305        let mut sensor = Sht3x::new(I2cScript::new(steps), I2C_ADDRESS_A, DelayLog::new());
306        assert_eq!(sensor.address(), I2C_ADDRESS_A);
307        assert_eq!(sensor.status(), None);
308        sensor.init().unwrap();
309        assert_eq!(sensor.status(), Some(Status::from_bits(Status::DEFAULT)));
310
311        let measurement = sensor.measure().unwrap();
312        assert_eq!(measurement, part);
313        assert_eq!(measurement.temperature_milli_celsius(), 25_000);
314        assert_eq!(measurement.humidity_milli_percent(), 60_000);
315
316        let (bus, delay) = sensor.release();
317        assert!(bus.done());
318        assert_eq!(
319            delay.waits_ns(),
320            [1_500_000, 1_000_000, 15_000_000],
321            "the 1.5 ms soft-reset time, the 1 ms command gap, then 15 ms at high repeatability"
322        );
323    }
324
325    #[test]
326    fn the_repeatability_picks_the_table_9_command_and_the_table_4_wait() {
327        let part = part_at(-10.5, 12.5);
328        let table = [
329            (Repeatability::Low, [0x24, 0x16], 4_000_000),
330            (Repeatability::Medium, [0x24, 0x0B], 6_000_000),
331            (Repeatability::High, [0x24, 0x00], 15_000_000),
332        ];
333        for (repeatability, command, wait_ns) in table {
334            let mut steps = init_steps(I2C_ADDRESS_B);
335            steps.extend(measure_steps(I2C_ADDRESS_B, command, part.to_bytes()));
336            let mut sensor = Sht3x::new(I2cScript::new(steps), I2C_ADDRESS_B, DelayLog::new())
337                .with_repeatability(repeatability);
338            assert_eq!(sensor.measure().unwrap(), part, "{repeatability:?}");
339            let (bus, delay) = sensor.release();
340            assert!(bus.done(), "{repeatability:?}");
341            assert_eq!(delay.waits_ns()[2], wait_ns, "{repeatability:?}");
342        }
343    }
344
345    #[test]
346    fn a_corrupted_word_is_a_crc_error() {
347        let mut data = part_at(25.0, 60.0).to_bytes();
348        data[4] ^= 0x80;
349        let mut steps = init_steps(I2C_ADDRESS_A);
350        steps.extend(measure_steps(I2C_ADDRESS_A, SINGLE_SHOT_HIGH, data));
351        let mut sensor = Sht3x::new(I2cScript::new(steps), I2C_ADDRESS_A, DelayLog::new());
352        assert_eq!(sensor.measure(), Err(DriverError::Sensor(SensorError::Crc)));
353        assert!(sensor.release().0.done());
354
355        let mut status = Status::from_bits(Status::DEFAULT).to_bytes();
356        status[2] ^= 0x01;
357        let steps = [
358            I2cStep::write(I2C_ADDRESS_A, SOFT_RESET),
359            I2cStep::write(I2C_ADDRESS_A, READ_STATUS),
360            I2cStep::read(I2C_ADDRESS_A, status),
361        ];
362        let mut sensor = Sht3x::new(I2cScript::new(steps), I2C_ADDRESS_A, DelayLog::new());
363        assert_eq!(sensor.init(), Err(DriverError::Sensor(SensorError::Crc)));
364        assert_eq!(
365            sensor.status(),
366            None,
367            "a word that fails its CRC is not kept"
368        );
369        assert!(sensor.release().0.done());
370    }
371
372    #[test]
373    fn a_bus_fault_surfaces_as_a_bus_error_and_maps_to_the_core_io_error() {
374        let kind = ErrorKind::NoAcknowledge(NoAcknowledgeSource::Address);
375        let steps = [I2cStep::fault(I2C_ADDRESS_A, kind)];
376        let mut sensor = Sht3x::new(I2cScript::new(steps), I2C_ADDRESS_A, DelayLog::new());
377        let error = sensor.init().unwrap_err();
378        assert!(matches!(error, DriverError::Bus(_)));
379        let core: pamoja_core::Error = error.into();
380        assert!(matches!(core, pamoja_core::Error::Io(_)));
381
382        let mut steps = init_steps(I2C_ADDRESS_A);
383        steps.push(I2cStep::write(I2C_ADDRESS_A, SINGLE_SHOT_HIGH));
384        steps.push(I2cStep::fault(I2C_ADDRESS_A, kind));
385        let mut sensor = Sht3x::new(I2cScript::new(steps), I2C_ADDRESS_A, DelayLog::new());
386        assert!(
387            matches!(sensor.measure(), Err(DriverError::Bus(_))),
388            "the NACK the part answers a read header with before data is ready is a bus error"
389        );
390        assert!(sensor.release().0.done());
391    }
392
393    #[test]
394    fn as_a_sensor_it_reads_and_maps_to_one_channel() {
395        let part = part_at(23.73, 63.37);
396        let mut steps = init_steps(I2C_ADDRESS_A);
397        steps.extend(measure_steps(
398            I2C_ADDRESS_A,
399            SINGLE_SHOT_HIGH,
400            part.to_bytes(),
401        ));
402        let sensor = Sht3x::new(I2cScript::new(steps), I2C_ADDRESS_A, DelayLog::new());
403        let mut celsius = sensor.map(|measurement| measurement.temperature_celsius());
404        let reading = block_on(celsius.read()).unwrap();
405        assert!((reading - 23.73).abs() < 0.002);
406        assert!(celsius.into_inner().release().0.done());
407    }
408
409    #[test]
410    fn the_heater_commands_are_table_16_and_the_status_shows_the_heater() {
411        let heating = Status::from_bits(Status::HEATER_ON);
412        let mut steps = init_steps(I2C_ADDRESS_A);
413        steps.extend([
414            I2cStep::write(I2C_ADDRESS_A, HEATER_ENABLE),
415            I2cStep::write(I2C_ADDRESS_A, READ_STATUS),
416            I2cStep::read(I2C_ADDRESS_A, heating.to_bytes()),
417            I2cStep::write(I2C_ADDRESS_A, HEATER_DISABLE),
418        ]);
419        let mut sensor = Sht3x::new(I2cScript::new(steps), I2C_ADDRESS_A, DelayLog::new());
420        sensor.heater_on().unwrap();
421        assert!(
422            !sensor.status().unwrap().heater_on(),
423            "the reset leaves the heater off"
424        );
425        assert_eq!(sensor.read_status().unwrap(), heating);
426        assert!(sensor.status().unwrap().heater_on());
427        sensor.heater_off().unwrap();
428        let (bus, delay) = sensor.release();
429        assert!(bus.done());
430        assert_eq!(
431            delay.waits_ns(),
432            [1_500_000, 1_000_000, 1_000_000, 1_000_000, 1_000_000],
433            "the reset time, then the 1 ms gap after each of the four commands"
434        );
435    }
436
437    #[test]
438    fn a_reset_and_one_measurement_make_the_transfers_the_datasheet_gives() {
439        use crate::sht3x::{
440            humidity_raw_from_relative_humidity, temperature_raw_from_celsius, Measurement, Status,
441            I2C_ADDRESS_A,
442        };
443
444        const PART: u8 = I2C_ADDRESS_A;
445        let status = Status::from_bits(Status::DEFAULT).to_bytes();
446        let data = Measurement {
447            temperature_raw: temperature_raw_from_celsius(25.0),
448            humidity_raw: humidity_raw_from_relative_humidity(60.0),
449        }
450        .to_bytes();
451        let bus = I2cScript::new([
452            I2cStep::write(PART, [0x30, 0xA2]),
453            I2cStep::write(PART, [0xF3, 0x2D]),
454            I2cStep::read(PART, status),
455            I2cStep::write(PART, [0x24, 0x00]),
456            I2cStep::read(PART, data),
457        ]);
458
459        let mut sensor = Sht3x::new(bus, PART, DelayLog::new());
460        let measurement = block_on(Sensor::read(&mut sensor)).expect("the scripted part answers");
461        assert_eq!(measurement.temperature_milli_celsius(), 25_000);
462        assert_eq!(measurement.humidity_milli_percent(), 60_000);
463
464        let (bus, delay) = sensor.release();
465        assert!(bus.done());
466        assert_eq!(delay.total_micros(), 1_500 + 1_000 + 15_000);
467    }
468}