dht_hal/
lib.rs

1//! An [`embedded-hal`] driver for the world's lousiest humidity/temperature
2//! sensors, the ubiquitous DHT11 (as seen in every beginners' Arduino kit and
3//! high-school science fair project ever) and its slightly more expensive
4//! cousin, the DHT22/AM2302.
5//!
6//! [`embedded-hal`]: https://crates.io/crates/embedded-hal
7#![no_std]
8use core::marker::PhantomData;
9use embedded_hal::{blocking::delay, digital::v2 as digital};
10pub mod kind;
11use self::kind::DhtKind;
12
13/// A DHT11 sensor.
14///
15/// These things are literally everywhere — you've definitely seen one and
16/// probably own several.
17///
18/// A DHT11 is a small, blue rectangle 15.5mm x 12mm on its face and 5.5mm wide.
19/// The DHT11 has a 1Hz sampling rate, meaning it can be read up to once every
20/// second. It supposedly works with 3.3V to 5V power and IO voltage — sometimes
21/// they have been known to not work when supplied 3.3v. It needs a 10 KOhm pullup
22/// resistor across the VCC and DATA pins, which most sellers will stick in the
23/// bag with it.
24///
25/// The DHT11 works between 20-80% relative humidity with 5% accuracy, and 0-50C
26/// with +- 2C accuracy. Which is to say...it's not a very good sensor. The
27/// primary advantage is that it is dirt cheap and about as common, which is why
28/// they can be found in every beginner's  electronics kit.
29pub type Dht11<P, T> = Dht<P, T, kind::Dht11>;
30
31/// A DHT22 (or AM2302, its wired variant) sensor.
32///
33/// This is the "luxury" version of the DHT series — DigiKey sells them for $10,
34/// which is twice as expensive as the DHT11. The extra $5 gets you relative
35/// humidity readings from 0-100% with 2-5% accuracy, and temperature readings
36/// from -40-80C with += 0.5C accuracy. This means that unless you actually
37/// don't care about measuring things, it's worth _significantly_ more than
38/// buying two DHT11s. However, it has an 0.5Hz sampling rate, meaning it can
39/// only be read once every two seconds. This is fine, because the numbers it
40/// gives you are actually meaningful, unlike the blue piece of garbage.
41///
42/// It has a white housing and is a bit larger than the DHT11, so all your
43/// friends will instantly be able to tell you're a big spender. It also needs
44/// 3.3V to 5V. The AM2302 is exactly the same sensor, but with 3 2cm leads
45/// rather than pins, and a fancy hole in the case so you can screw it onto
46/// something. Whether or not this is worth another $5 is up to you, but it does
47/// have the advantage of not having to remember which of the 4 pins these
48/// things have "goes nowhere and does nothing".
49///
50/// Unlike the DHT11, these often (but not always!) have a 10K pullup resistor
51/// inside the housing. Unfortunately, there's no real way to tell whether or
52/// not you're one of the lucky ones, so you should probably add one anyway.
53/// Welcome to the wonderful world of cheap electronics components from China!
54pub type Dht22<P, T> = Dht<P, T, kind::Dht22>;
55
56/// A generic DHT-series sensor.
57///
58/// Currently, this supports the DHT11 and DHT22/AM2302.
59#[derive(Debug)]
60pub struct Dht<P, T, K> {
61    pin: P,
62    timer: T,
63    _kind: PhantomData<K>,
64}
65
66/// A DHT sensor combined temperature and relative humidity reading.
67#[derive(Debug, Clone)]
68pub struct Reading<K> {
69    rh_integral: u8,
70    rh_decimal: u8,
71    t_integral: u8,
72    t_decimal: u8,
73    _kind: PhantomData<fn(K)>,
74}
75
76#[derive(Eq, PartialEq, Debug)]
77pub struct Error<I>(ErrorKind<I>);
78
79#[derive(Eq, PartialEq, Debug)]
80enum ErrorKind<I> {
81    Io(I),
82    Checksum { expected: u8, actual: u8 },
83    Timeout,
84}
85
86#[derive(Copy, Clone, Debug)]
87struct Pulse {
88    lo: u8,
89    hi: u8,
90}
91
92impl<P, T, K, E> Dht<P, T, K>
93where
94    P: digital::InputPin<Error = E> + digital::OutputPin<Error = E>,
95    K: DhtKind,
96{
97    /// Returns a new DHT sensor.
98    pub fn new(pin: P, timer: T) -> Self {
99        Self {
100            pin,
101            timer,
102            _kind: PhantomData,
103        }
104    }
105}
106impl<P, T, K, E> Dht<P, T, K>
107where
108    P: digital::InputPin<Error = E> + digital::OutputPin<Error = E>,
109    T: delay::DelayUs<u16> + delay::DelayMs<u16>,
110    K: DhtKind,
111{
112    #[inline(always)] // timing-critical
113    fn read_pulse_us(&mut self, high: bool) -> Result<u8, ErrorKind<E>> {
114        for len in 0..=core::u8::MAX {
115            if self.pin.is_high()? != high {
116                return Ok(len);
117            }
118            self.timer.delay_us(1);
119        }
120        Err(ErrorKind::Timeout)
121    }
122
123    fn start_signal_blocking(&mut self) -> Result<(), ErrorKind<E>> {
124        // set pin high for 1 ms to pull up.
125        self.pin.set_high()?;
126        self.timer.delay_ms(1);
127
128        // send start signal
129        self.pin.set_low()?;
130        self.timer.delay_us(K::START_DELAY_US);
131        // end start signal
132        self.pin.set_high()?;
133        self.timer.delay_us(40);
134
135        // Wait for an ~80ms low pulse, followed by an ~80ms high pulse.
136        self.read_pulse_us(false)?;
137        self.read_pulse_us(true)?;
138
139        Ok(())
140    }
141
142    /// Read from the DHT sensor using blocking delays.
143    ///
144    /// Note that this is timing-critical, and should be run with interrupts disabled.
145    pub fn read_blocking(&mut self) -> Result<Reading<K>, Error<E>> {
146        self.start_signal_blocking().map_err(ErrorKind::from)?;
147
148        // The sensor will now send us 40 bits of data. For each bit, the sensor
149        // will assert the line low for 50 microseconds as a delimiter, and then
150        // will assert the line high for a variable-length pulse to encode the
151        // bit. If the high pulse is 70 us long, then the bit is 1, and if it is
152        // 28 us, then the bit is a 0.
153        //
154        // Because timing is sloppy, we will read each bit by comparing the
155        // length of the initial low pulse with the length of the following high
156        // pulse. If it was longer than the 50us low pulse, then it's closer to
157        // 70us, and if it was shorter, than it is closer to 28 us.
158        let mut pulses = [Pulse { lo: 0, hi: 0 }; 40];
159
160        // Read each bit from the sensor now. We'll convert the raw pulses into
161        // bytes in a subsequent step, to avoid doing that work in the
162        // timing-critical loop.
163        for pulse in &mut pulses[..] {
164            pulse.lo = self.read_pulse_us(false)?;
165            pulse.hi = self.read_pulse_us(true)?;
166        }
167        Ok(Reading::from_pulses(&pulses)?)
168    }
169}
170
171impl<K: DhtKind> Reading<K> {
172    fn from_pulses<E>(pulses: &[Pulse; 40]) -> Result<Self, ErrorKind<E>> {
173        let mut bytes = [0u8; 5];
174        // The last byte sent by the sensor is a checksum, which should be the
175        // low byte of the 16-bit sum of the first four data bytes.
176        let mut chksum: u16 = 0;
177        for (i, pulses) in pulses.chunks(8).enumerate() {
178            let byte = &mut bytes[i];
179            // If the high pulse is longer than the leading low pulse, the bit
180            // is a 1, otherwise, it's a 0.
181            for Pulse { lo, hi } in pulses {
182                *byte <<= 1;
183                if hi > lo {
184                    *byte |= 1;
185                }
186            }
187            // If this isn't the last byte, then add it to the checksum.
188            if i < 4 {
189                chksum += i as u16;
190            }
191        }
192
193        // Does the checksum match?
194        let expected = bytes[4];
195        let actual = chksum as u8;
196        if actual != expected {
197            return Err(ErrorKind::Checksum { actual, expected });
198        }
199
200        Ok(Self {
201            rh_integral: bytes[0],
202            rh_decimal: bytes[1],
203            t_integral: bytes[2],
204            t_decimal: bytes[3],
205            _kind: PhantomData,
206        })
207    }
208
209    /// Returns the temperature in Celcius.
210    pub fn temp_celcius(self) -> f32 {
211        K::temp_celcius(self.t_integral, self.t_decimal)
212    }
213
214    /// Returns the temperature in Fahrenheit.
215    pub fn temp_fahrenheit(self) -> f32 {
216        celcius_to_fahrenheit(self.temp_celcius())
217    }
218
219    /// Returns the temperature in Fahrenheit.
220    pub fn humidity_percent(self) -> f32 {
221        K::humidity_percent(self.rh_integral, self.rh_decimal)
222    }
223}
224
225impl<E> From<E> for ErrorKind<E> {
226    fn from(e: E) -> Self {
227        ErrorKind::Io(e)
228    }
229}
230
231// === impl Error ===
232
233impl<E> From<ErrorKind<E>> for Error<E> {
234    fn from(e: ErrorKind<E>) -> Self {
235        Self(e)
236    }
237}
238
239impl<E> Error<E> {
240    /// Returns `true` if a read from the sensor timed out.
241    pub fn is_timeout(&self) -> bool {
242        match self.0 {
243            ErrorKind::Timeout => true,
244            _ => false,
245        }
246    }
247
248    /// Returns `true` if an IO error occurred while reading from or writing to
249    /// the sensor's data pin.
250    pub fn is_io(&self) -> bool {
251        match self.0 {
252            ErrorKind::Io(_) => true,
253            _ => false,
254        }
255    }
256
257    /// Returns `true` if the reading from the sensor had a bad checksum.
258    pub fn is_checksum(&self) -> bool {
259        match self.0 {
260            ErrorKind::Checksum { .. } => true,
261            _ => false,
262        }
263    }
264
265    /// If the error was caused by an underlying pin IO error, returns it.
266    pub fn into_io(self) -> Option<E> {
267        match self.0 {
268            ErrorKind::Io(io) => Some(io),
269            _ => None,
270        }
271    }
272}
273
274fn celcius_to_fahrenheit(c: f32) -> f32 {
275    c * 1.8 + 32.0
276}