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}