aquarium_control/sensors/dht.rs
1/* Copyright 2024 Uwe Martin
2
3Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
5The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
7THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8*/
9use log::error;
10#[cfg(all(not(test), target_os = "linux"))]
11use log::info;
12
13use crate::sensors::dht_channels::DhtChannels;
14use crate::sensors::dht_config::DhtConfig;
15use crate::sensors::dht_error::DhtError;
16use crate::utilities::check_mutex_access_duration::CheckMutexAccessDurationTrait;
17#[cfg(not(test))]
18use crate::utilities::logger::log_error_chain;
19
20use crate::utilities::proc_ext_req::ProcessExternalRequestTrait;
21#[cfg(all(not(test), target_os = "linux"))]
22use nix::unistd::gettid;
23use spin_sleep::SpinSleeper;
24use std::sync::{Arc, Mutex};
25use std::time::{Duration, Instant};
26
27cfg_if::cfg_if! {
28 if #[cfg(all(target_os = "linux", feature = "target_hw"))] {
29 use rppal::gpio::{Gpio, Level, Trigger};
30 use std::thread::{self, sleep};
31
32 #[cfg(feature = "debug_dht")]
33 use log::debug;
34 }
35 else {
36 use crate::sensors::gpio_handler::Gpio;
37 }
38}
39
40/// Type definition to avoid linter warnings about complex return types
41pub type DhtResult = Result<(f32, f32), DhtError>; // Temperature and Humidity
42
43/// allow max. 10 milliseconds for mutex to be blocked by any other thread
44const MAX_MUTEX_ACCESS_DURATION_MILLIS: u64 = 10;
45
46/// Contains constants for communication with the DHT22 sensor.
47pub mod dht22_constants {
48 #[allow(unused)]
49 pub const DHT22_DATA_LEN: usize = 5;
50 #[allow(unused)]
51 pub const DHT22_BYTE0_START_IDX: usize = 1;
52 #[allow(unused)]
53 pub const DHT22_BYTE0_STOP_IDX: usize = 8;
54 #[allow(unused)]
55 pub const DHT22_BYTE1_START_IDX: usize = 9;
56 #[allow(unused)]
57 pub const DHT22_BYTE1_STOP_IDX: usize = 16;
58 #[allow(unused)]
59 pub const DHT22_BYTE2_START_IDX: usize = 17;
60 #[allow(unused)]
61 pub const DHT22_BYTE2_STOP_IDX: usize = 23;
62 #[allow(unused)]
63 pub const DHT22_BYTE3_START_IDX: usize = 24;
64 #[allow(unused)]
65 pub const DHT22_BYTE3_STOP_IDX: usize = 31;
66 #[allow(unused)]
67 pub const DHT22_BYTE4_START_IDX: usize = 32;
68 #[allow(unused)]
69 pub const DHT22_BYTE4_STOP_IDX: usize = 39;
70
71 #[allow(unused)]
72 pub const DHT22_TRANSMISSION_END_IDX: usize = 86;
73
74 #[allow(unused)]
75 pub const DHT22_HUMIDITY_START_IDX: usize = 1;
76 #[allow(unused)]
77 pub const DHT22_HUMIDITY_STOP_IDX: usize = 16;
78
79 #[allow(unused)]
80 pub const DHT22_TEMPERATURE_START_IDX: usize = 1;
81 #[allow(unused)]
82 pub const DHT22_TEMPERATURE_STOP_IDX: usize = 16;
83
84 // minimum and maximum value for temperature10 range checks
85 #[allow(unused)]
86 pub const DHT22_MINVAL_TEMP10: u32 = 0;
87 #[allow(unused)]
88 pub const DHT22_MAXVAL_TEMP10: u32 = 500;
89
90 // minimum and maximum value for temperature10 range checks
91 #[allow(unused)]
92 pub const DHT22_MINVAL_HUM10: u32 = 0;
93 #[allow(unused)]
94 pub const DHT22_MAXVAL_HUM10: u32 = 1000;
95
96 // timing of (neutral) low phase between data bit
97 #[allow(unused)]
98 pub const DHT22_NEUTRAL_LEN_MIN: u64 = 30;
99 #[allow(unused)]
100 pub const DHT22_NEUTRAL_LEN_MAX: u64 = 60;
101
102 // distinguish between true and false
103 #[allow(unused)]
104 pub const DHT22_BIT_TIMING_LIMIT: u64 = 50;
105
106 // maximum number of allowed retries to get valid data
107 #[allow(unused)]
108 pub const DHT22_MAX_RETRIES: u64 = 5;
109
110 /// allow max. 10 milliseconds for mutex to be blocked by any other thread
111 #[allow(unused)]
112 pub const MAX_MUTEX_ACCESS_DURATION_MILLIS: u64 = 10;
113}
114
115#[cfg_attr(doc, aquamarine::aquamarine)]
116/// Represents a Digital Humidity and Temperature (DHT) sensor module, typically a DHT22,
117/// responsible for reading ambient temperature and humidity.
118///
119/// This struct encapsulates the configuration, GPIO interface details, and internal state
120/// required to communicate with a DHT sensor. It handles the low-level GPIO signaling
121/// to read data, apply retries, and perform post-processing (checksum validation,
122/// signal calculation) before providing the final temperature and humidity values.
123///
124/// The communication with the DHT sensor is highly timing-sensitive and involves
125/// precise GPIO manipulation, including setting pin states, managing delays, and
126/// capturing pulse durations via interrupts.
127///
128/// Thread Communication:
129/// This module is designed to run in its own dedicated thread, periodically reading sensor data
130/// and making the latest measurements available via a shared `Arc<Mutex<DhtResult>>`.
131/// Other modules (like `SensorManager` or `DataLogger`) can then read from this mutex to get the
132/// current temperature and humidity. It also responds to `Quit` commands for graceful shutdown.
133///
134/// Platform-Specific Behavior:
135/// The core sensor reading logic (`read` function) is conditionally compiled:
136/// - On Linux with the `target_hw` feature enabled, it directly uses `rppal::gpio` for hardware interaction.
137/// - On other platforms or when `target_hw` is disabled, it uses a stub implementation (`Err(DhtError::IncorrectPlatform)`)
138/// to allow compilation and testing without physical hardware.
139///
140/// Thread communication of this component is as follows:
141/// ```mermaid
142/// graph LR
143/// dht[Dht] -.-> SensorManager[Sensor Managerr]
144/// signal_handler[SignalHandler] --> dht
145/// ```
146pub struct Dht {
147 #[allow(unused)]
148 /// configuration for communication with DHT sensor
149 config: DhtConfig,
150
151 /// GPIO interface
152 #[allow(unused)]
153 gpio_opt: Option<Gpio>,
154
155 #[allow(unused)]
156 /// numeric GPIO pin for communication with DHT22 sensor
157 gpio_dht22_io: u8,
158
159 /// numeric GPIO pin for providing power to the DHT22 sensor
160 #[allow(unused)]
161 gpio_dht22_vcc: u8,
162
163 /// time instance of the last recording
164 #[allow(unused)] // used in conditionally compiled code
165 instant_last_measurement: Instant,
166
167 /// flag to avoid flooding the log file in case mutex cannot be locked
168 #[allow(unused)] // used in conditionally compiled code
169 lock_error_mutex: bool,
170
171 /// inhibition flag to avoid flooding the log file with repeated messages about excessive access time to mutex
172 #[allow(unused)] // used in conditionally compiled code
173 pub(crate) lock_warn_max_mutex_access_duration: bool,
174
175 /// inhibition flag to avoid flooding the log file with repeated messages about failure to read from DHT sensor
176 #[allow(unused)] // used in conditionally compiled code
177 lock_error_failed_after_retries: bool,
178
179 // for testing purposes: record when mutex access time is exceeded without resetting it
180 #[cfg(test)]
181 pub(crate) mutex_access_duration_exceeded: bool,
182
183 /// Maximum permissible access duration for Mutex
184 pub max_mutex_access_duration: Duration,
185
186 /// Duration between measurements
187 pub measurement_interval: Duration,
188}
189
190impl Dht {
191 /// Creates a new **`Dht`** instance for interacting with a DHT22 sensor on the target platform.
192 ///
193 /// This constructor initializes the DHT sensor module, configuring it with the provided
194 /// settings and gaining access to the necessary GPIO pins for communication and power supply
195 /// to the DHT22 sensor. This specific implementation is enabled only for Linux targets
196 /// when the `target_hw` feature is active.
197 ///
198 /// # Arguments
199 /// * `config` - **Configuration data** for the DHT sensor, including timing parameters for communication.
200 /// * `gpio_opt` - A **`Gpio` instance** from `rppal`, providing the interface to the system's GPIO pins, wrapped in Option.
201 /// * `gpio_dht22_io` - The **GPIO pin** used for data input/output with the DHT22 sensor.
202 /// * `gpio_dht22_vcc` - The **GPIO pin** used to control the power supply (VCC) to the DHT22 sensor.
203 ///
204 /// # Returns
205 /// A new **`Dht` struct**, ready to read temperature and humidity.
206 #[allow(unused)] // used in conditionally compiled code
207 pub fn new(
208 config: DhtConfig,
209 gpio_opt: Option<Gpio>,
210 gpio_dht22_io: u8,
211 gpio_dht22_vcc: u8,
212 ) -> Dht {
213 let measurement_interval = Duration::from_millis(config.measurement_interval_millis);
214 Dht {
215 config,
216 gpio_opt,
217 gpio_dht22_io,
218 gpio_dht22_vcc,
219 instant_last_measurement: Instant::now(),
220 lock_error_mutex: false,
221 lock_warn_max_mutex_access_duration: false,
222 lock_error_failed_after_retries: false,
223
224 #[cfg(test)]
225 mutex_access_duration_exceeded: false,
226 max_mutex_access_duration: Duration::from_millis(MAX_MUTEX_ACCESS_DURATION_MILLIS),
227 measurement_interval,
228 }
229 }
230
231 #[allow(unused)]
232 /// Calculates the raw data bits from the timing sequence of GPIO signal changes during DHT22 sensor communication.
233 ///
234 /// This private helper function interprets the pulse widths measured from the DHT22 sensor.
235 /// It differentiates between "neutral" pauses (which are ignored) and actual data bits
236 /// (distinguished by their duration relative to `DHT22_BIT_TIMING_LIMIT`).
237 /// Longer pulses are interpreted as `true` bits, shorter as `false` bits.
238 ///
239 /// # Arguments
240 /// * `ticks` - An array of `u64` values, where each value represents the duration (in microseconds)
241 /// of a specific pulse or pause observed on the data line.
242 /// * `valid_ticks_received` - The actual number of valid (non-timeout) interrupt events captured,
243 /// indicating how many entries in `ticks` are relevant.
244 ///
245 /// # Returns
246 /// A fixed-size array of `bool` representing the extracted data bits from the sensor.
247 /// This array contains `true` for a '1' bit and `false` for a '0' bit.
248 pub fn calc_data_bits(
249 ticks: [u64; dht22_constants::DHT22_TRANSMISSION_END_IDX],
250 valid_ticks_received: usize,
251 ) -> [bool; dht22_constants::DHT22_BYTE4_STOP_IDX + 1] {
252 let mut data_bits = [false; dht22_constants::DHT22_BYTE4_STOP_IDX + 1];
253 let mut count_data_bits = 0;
254 for tick in ticks.iter().take(valid_ticks_received + 1) {
255 if tick > &dht22_constants::DHT22_NEUTRAL_LEN_MIN
256 && tick < &dht22_constants::DHT22_NEUTRAL_LEN_MAX
257 {
258 continue;
259 }
260 if tick > &dht22_constants::DHT22_BIT_TIMING_LIMIT {
261 data_bits[count_data_bits] = true;
262 }
263 count_data_bits += 1;
264 if count_data_bits > dht22_constants::DHT22_BYTE4_STOP_IDX {
265 break;
266 }
267 }
268 data_bits
269 }
270
271 #[allow(unused)]
272 /// Converts a specific range of raw boolean data bits into a single `u8` byte.
273 ///
274 /// This private helper function takes a segment of the `data_bits` array and reconstructs
275 /// the corresponding byte by iterating through the bits and shifting them into place.
276 ///
277 /// # Arguments
278 /// * `data_bits` - The array of `bool` representing the raw data bits received from the sensor.
279 /// * `start_index` - The starting index (inclusive) within `data_bits` for the byte's data.
280 /// * `stop_index` - The ending index (inclusive) within `data_bits` for the byte's data.
281 ///
282 /// # Returns
283 /// A `u8` representing the reconstructed byte.
284 pub fn calc_byte(
285 data_bits: [bool; dht22_constants::DHT22_BYTE4_STOP_IDX + 1],
286 start_index: usize,
287 stop_index: usize,
288 ) -> u8 {
289 // `fold` iterates through the bits, accumulating the final byte value.
290 data_bits[start_index..=stop_index]
291 .iter()
292 .fold(0u8, |accumulator, &bit| (accumulator << 1) | (bit as u8))
293 }
294
295 #[allow(unused)]
296 /// Arranges the raw boolean bit information received from the DHT22 sensor into a 5-byte array.
297 ///
298 /// This private helper function takes the array of individual data bits (extracted from pulse timings)
299 /// and reconstructs the full data payload from the sensor, byte by byte, according to the
300 /// DHT22 communication protocol.
301 ///
302 /// # Arguments
303 /// * `data_bits` - The fixed-size array of `bool` representing the raw data bits received from the sensor.
304 ///
305 /// # Returns
306 /// A fixed-size array of `u8` (5 bytes) containing the reconstructed sensor data,
307 /// ready for checksum validation and signal calculation.
308 pub fn calc_byte_array(
309 data_bits: [bool; dht22_constants::DHT22_BYTE4_STOP_IDX + 1],
310 ) -> [u8; dht22_constants::DHT22_DATA_LEN] {
311 let mut byte_vals = [0u8; dht22_constants::DHT22_DATA_LEN];
312
313 byte_vals[0] = Self::calc_byte(
314 data_bits,
315 dht22_constants::DHT22_BYTE0_START_IDX,
316 dht22_constants::DHT22_BYTE0_STOP_IDX,
317 );
318
319 byte_vals[1] = Self::calc_byte(
320 data_bits,
321 dht22_constants::DHT22_BYTE1_START_IDX,
322 dht22_constants::DHT22_BYTE1_STOP_IDX,
323 );
324
325 byte_vals[2] = Self::calc_byte(
326 data_bits,
327 dht22_constants::DHT22_BYTE2_START_IDX,
328 dht22_constants::DHT22_BYTE2_STOP_IDX,
329 );
330
331 byte_vals[3] = Self::calc_byte(
332 data_bits,
333 dht22_constants::DHT22_BYTE3_START_IDX,
334 dht22_constants::DHT22_BYTE3_STOP_IDX,
335 );
336
337 byte_vals[4] = Self::calc_byte(
338 data_bits,
339 dht22_constants::DHT22_BYTE4_START_IDX,
340 dht22_constants::DHT22_BYTE4_STOP_IDX,
341 );
342
343 byte_vals
344 }
345
346 #[allow(unused)]
347 /// Calculates the checksum of the DHT22 sensor data and validates it against the received checksum.
348 ///
349 /// This private helper function computes an 8-bit sum (modulo 256) of the first four data bytes
350 /// received from the DHT22 sensor. It then compares this calculated checksum with the checksum
351 /// byte provided by the sensor itself.
352 ///
353 /// If the checksums do not match, a warning or error is logged (depending on build configuration),
354 /// and a `DhtError` is returned, indicating data corruption during transmission.
355 ///
356 /// # Arguments
357 /// * `byte_vals` - A fixed-size array of `u8` (5 bytes) containing the sensor's raw data,
358 /// where the last byte is the received checksum.
359 /// * `try_again` - A mutable reference to a boolean flag. Set to `true` when the top-level calling function may try again.
360 ///
361 /// # Returns
362 /// An empty `Result` (`Ok(())`) if the calculated checksum matches the received one.
363 ///
364 /// # Errors
365 /// Returns a `DhtError` if the checksum is invalid:
366 /// - `DhtError::ChecksumError`: If the calculated checksum does not match the received checksum,
367 /// indicating that the data was corrupted during transmission. This is considered a
368 /// recoverable error, and the calling function should retry the read operation.
369 pub fn calc_checksum(
370 byte_vals: [u8; dht22_constants::DHT22_DATA_LEN],
371 try_again: &mut bool,
372 ) -> Result<u8, DhtError> {
373 // Use `wrapping_add` for a clear and correct 8-bit sum.
374 let checksum_calculated = byte_vals[0]
375 .wrapping_add(byte_vals[1])
376 .wrapping_add(byte_vals[2])
377 .wrapping_add(byte_vals[3]);
378
379 let checksum_received: u8 = byte_vals[4];
380 if checksum_calculated != checksum_received {
381 *try_again = true;
382 return Err(DhtError::ChecksumError(
383 module_path!().to_string(),
384 checksum_calculated,
385 checksum_received,
386 ));
387 }
388 Ok(checksum_calculated)
389 }
390
391 #[allow(unused)]
392 /// Calculates final temperature and humidity values from the raw byte array.
393 ///
394 /// This function reconstructs the 16-bit integer values for humidity and temperature
395 /// from their respective byte pairs. It then performs range checks to ensure the values
396 /// are within the sensor's specified operating limits before converting them into
397 /// standard `f32` floating-point representations.
398 ///
399 /// # Arguments
400 /// * `byte_vals` - A fixed-size array of `u8` (5 bytes) containing the validated sensor data.
401 ///
402 /// # Returns
403 /// A `Result` containing a tuple `(f32, f32)` with the temperature in °C and humidity in %RH.
404 ///
405 /// # Errors
406 /// Returns a `DhtError` if a value is outside the sensor's valid operating range:
407 /// - `DhtError::TemperatureOutOfRange`: If the calculated temperature is invalid.
408 /// - `DhtError::HumidityOutOfRange`: If the calculated humidity is invalid.
409 pub fn calc_signals(
410 byte_vals: [u8; dht22_constants::DHT22_DATA_LEN],
411 ) -> Result<(f32, f32), DhtError> {
412 // calculate integer value for humidity * 10
413 let mut hum10: u32 = byte_vals[0].into();
414 hum10 <<= 8;
415 hum10 += byte_vals[1] as u32;
416
417 // calculate integer value for temperature * 10
418 let mut temp10: u32 = byte_vals[2].into();
419 temp10 <<= 8;
420 temp10 += byte_vals[3] as u32;
421
422 // range check for temperature
423 if temp10 > dht22_constants::DHT22_MAXVAL_TEMP10 {
424 return Err(DhtError::TemperatureOutOfRange(
425 module_path!().to_string(),
426 (temp10 / 10) as f32,
427 (dht22_constants::DHT22_MINVAL_TEMP10 / 10) as f32,
428 (dht22_constants::DHT22_MAXVAL_TEMP10 / 10) as f32,
429 ));
430 }
431
432 // range check for humidity
433 if hum10 > dht22_constants::DHT22_MAXVAL_HUM10 {
434 return Err(DhtError::HumidityOutOfRange(
435 module_path!().to_string(),
436 (hum10 / 10) as f32,
437 (dht22_constants::DHT22_MINVAL_HUM10 / 10) as f32,
438 (dht22_constants::DHT22_MAXVAL_HUM10 / 10) as f32,
439 ));
440 }
441
442 // calculate final values
443 let temperature: f32 = (temp10 as f32) / 10.0;
444 let humidity: f32 = (hum10 as f32) / 10.0;
445
446 Ok((temperature, humidity))
447 }
448
449 #[cfg(all(target_os = "linux", feature = "target_hw"))]
450 /// Performs a low-level read from a DHT22 sensor using the `rppal` library.
451 ///
452 /// This function directly interacts with the GPIO pins to implement the DHT22 communication protocol.
453 /// It handles sending the start signal, setting up interrupts to capture response timings,
454 /// and then processing these timings into a final result.
455 ///
456 /// # Arguments
457 /// * `reset_sensor` - If `true`, the sensor's power (VCC) will be toggled to perform a
458 /// hardware reset before the reading.
459 ///
460 /// # Returns
461 /// A `Result` containing a tuple `(f32, f32)` with the temperature and humidity on success.
462 ///
463 /// # Errors
464 /// Returns a `DhtError` for a wide range of hardware and protocol failures:
465 /// - `DhtError::VccPinAcquisitionFailed`: If the VCC GPIO pin cannot be acquired.
466 /// - `DhtError::DataPinOutputAcquisitionFailed`: If the data GPIO pin cannot be acquired for output.
467 /// - `DhtError::DataPinInputAcquisitionFailed`: If the data GPIO pin cannot be acquired for input.
468 /// - `DhtError::CouldNotSetInterrupt`: If setting the GPIO interrupt fails.
469 /// - `DhtError::InterruptFailed`: If an error occurs during interrupt polling.
470 /// - `DhtError::Timeout`: If the sensor does not respond within the expected time frame.
471 /// - `DhtError::WrongNumberOfValidTicks`: If the number of signal transitions received is incorrect.
472 /// - `DhtError::ChecksumCalculationError`: If an error occurs during the checksum calculation.
473 /// - `DhtError::ChecksumError`: If the calculated checksum does not match the received checksum.
474 /// - `DhtError::TemperatureOutOfRange`: If the measured temperature is outside the sensor's specified range.
475 /// - `DhtError::HumidityOutOfRange`: If the measured humidity is outside the sensor's specified range.
476 /// - `DhtError::IncorrectPlatform`: If called with no gpio lib handle available.
477 pub fn read(&mut self, reset_sensor: bool, try_again: &mut bool) -> DhtResult {
478 let mut ticks = [0u64; dht22_constants::DHT22_TRANSMISSION_END_IDX];
479
480 let gpio: &mut Gpio = if self.gpio_opt.is_some() {
481 self.gpio_opt.as_mut().unwrap()
482 } else {
483 return Err(DhtError::GpioHandleNotProvided(module_path!().to_string()));
484 };
485
486 // get pin for providing supply voltage to DHT sensor
487 let mut dht_vcc_pin = match gpio.get(self.gpio_dht22_vcc) {
488 Ok(c) => c.into_output(),
489 Err(e) => {
490 return Err(DhtError::VccPinAcquisitionFailed {
491 location: module_path!().to_string(),
492 vcc_pin_nr: self.gpio_dht22_vcc,
493 source: e,
494 });
495 }
496 };
497
498 // scope to force the drop of the output pin
499 {
500 // get pin for triggering DHT sensor
501 let mut data_pin = match gpio.get(self.gpio_dht22_io) {
502 Ok(c) => c.into_output(),
503 Err(e) => {
504 return Err(DhtError::DataPinOutputAcquisitionFailed {
505 location: module_path!().to_string(),
506 data_pin_nr: self.gpio_dht22_io,
507 source: e,
508 });
509 }
510 };
511
512 // reset of DHT sensor
513 if reset_sensor {
514 dht_vcc_pin.set_low();
515 thread::sleep(Duration::from_millis(
516 self.config.sensor_reset_duration_millis,
517 ));
518 dht_vcc_pin.set_high();
519 thread::sleep(Duration::from_millis(
520 self.config.sensor_startup_duration_millis,
521 ));
522 // give the sensor some time to wake up
523 } else {
524 thread::sleep(Duration::from_millis(
525 self.config.sensor_pause_duration_millis,
526 ));
527 // pause between transmissions
528 }
529
530 // send start signal
531 data_pin.write(Level::Low);
532 sleep(Duration::from_micros(
533 self.config.sensor_init_pin_down_duration_micros,
534 ));
535 data_pin.write(Level::High);
536 sleep(Duration::from_micros(
537 self.config.sensor_init_pin_up_duration_micros,
538 ));
539 }
540
541 // get pin for reading from DHT sensor
542 let mut data_pin = match gpio.get(self.gpio_dht22_io) {
543 Ok(c) => c.into_input_pullup(),
544 Err(e) => {
545 return Err(DhtError::DataPinInputAcquisitionFailed {
546 location: module_path!().to_string(),
547 input_pin_nr: self.gpio_dht22_io,
548 source: e,
549 });
550 }
551 };
552
553 // The interrupt shall be triggered for both, falling and rising edge.
554 // Debouncing does not work, hence no (None) debouncing time provided
555 match data_pin.set_interrupt(Trigger::Both, None) {
556 Ok(()) => {}
557 Err(_) => {
558 return Err(DhtError::CouldNotSetInterrupt(module_path!().to_string()));
559 }
560 }
561
562 let timeout_duration = Duration::from_micros(self.config.timeout_duration_micros);
563
564 let mut timeout_occurred = false;
565 let mut valid_ticks_received: usize = 0;
566
567 // initialization to avoid compiler error
568 let mut duration_before: Duration = Duration::from_micros(0);
569 let mut duration_after: Duration;
570
571 // observe level changes and record the timing
572 for i in 0..dht22_constants::DHT22_TRANSMISSION_END_IDX {
573 // poll interrupt
574 let event_opt = match data_pin.poll_interrupt(false, Some(timeout_duration)) {
575 Ok(c) => c,
576 Err(_) => {
577 return Err(DhtError::InterruptFailed(module_path!().to_string()));
578 }
579 };
580
581 // evaluate the recorded timestamp
582 match event_opt {
583 Some(event) => {
584 duration_after = event.timestamp;
585 }
586 None => {
587 duration_after = duration_before;
588 }
589 }
590
591 if i == 0 {
592 // for the first bit timing, no start data is available
593 ticks[0] = 0;
594 } else {
595 let bit_duration = duration_after - duration_before;
596 ticks[i] = bit_duration.as_micros() as u64;
597 }
598 duration_before = duration_after;
599
600 // abort measurement if timeout occurred
601 if ticks[i] > self.config.timeout_duration_micros {
602 timeout_occurred = true;
603 break;
604 }
605 valid_ticks_received = i;
606 }
607 match data_pin.clear_interrupt() {
608 Ok(()) => {}
609 Err(_) => {
610 return Err(DhtError::CouldNotClearInterrupt(module_path!().to_string()));
611 }
612 }
613
614 if timeout_occurred {
615 *try_again = true;
616 return Err(DhtError::Timeout(module_path!().to_string()));
617 }
618
619 if valid_ticks_received != dht22_constants::DHT22_TRANSMISSION_END_IDX - 1 {
620 return Err(DhtError::WrongNumberOfValidTicks(
621 module_path!().to_string(),
622 dht22_constants::DHT22_TRANSMISSION_END_IDX - 1,
623 valid_ticks_received,
624 ));
625 }
626
627 // identify the data bits and store values in data_bits
628 let data_bits = Self::calc_data_bits(ticks, valid_ticks_received);
629
630 let byte_vals = Self::calc_byte_array(data_bits);
631
632 // checksum calculation
633 match Self::calc_checksum(byte_vals, try_again) {
634 Ok(_) => { /* do nothing */ }
635 Err(e) => return Err(e),
636 }
637
638 // calculate signals (or error code) and directly return it
639 Self::calc_signals(byte_vals)
640 }
641
642 #[allow(unused)] // used in conditionally compiled code
643 #[cfg(not(all(target_os = "linux", feature = "target_hw")))]
644 /// Stub function for non-target platforms to allow compilation.
645 ///
646 /// This version is used when the `target_hw` feature is disabled or on a non-Linux OS.
647 /// It does not interact with hardware and always returns an error.
648 ///
649 /// # Returns
650 /// This function does not return a value.
651 ///
652 /// # Errors
653 /// Always returns `Err(DhtError::IncorrectPlatform)`.
654 pub fn read(&self, _reset_sensor: bool, _try_again: &mut bool) -> DhtResult {
655 Err(DhtError::IncorrectPlatform(module_path!().to_string()))
656 }
657
658 /// Initiates a robust reading process from the DHT22 sensor, with retries.
659 ///
660 /// This function attempts to read temperature and humidity data. It includes an
661 /// internal retry mechanism for transient, recoverable errors like timeouts or
662 /// checksum mismatches, making the sensor reading more reliable.
663 ///
664 /// # Returns
665 /// A `Result` containing a tuple `(f32, f32)` with the temperature and humidity on success.
666 ///
667 /// # Errors
668 /// Returns a `DhtError` if the reading fails permanently or after all retries:
669 /// - `DhtError::FailedAfterRetries`: If all attempts fail due to recoverable errors.
670 /// - Any other `DhtError` variant for non-recoverable hardware or configuration failures.
671 #[allow(unused)] // used in conditionally compiled code
672 pub fn get_data(&mut self) -> DhtResult {
673 for i in 0..=dht22_constants::DHT22_MAX_RETRIES {
674 let mut try_again = false;
675 // Reset power supply only on the first attempt (i == 0).
676 let read_result = self.read(i == 0, &mut try_again);
677
678 match read_result {
679 // On success, return immediately.
680 Ok(data) => return Ok(data),
681 Err(e) => {
682 // If the error is not recoverable, return it immediately.
683 if !try_again {
684 return Err(e);
685 }
686 // If this was the last retry, and it failed, return FailedAfterRetries.
687 if i == dht22_constants::DHT22_MAX_RETRIES {
688 return Err(DhtError::FailedAfterRetries {
689 location: module_path!().to_string(),
690 retry_counter: i,
691 source: Box::new(e),
692 });
693 }
694 // Otherwise, the loop will continue for another retry.
695 }
696 }
697 }
698 unreachable!();
699 }
700
701 /// Executes the main loop for the DHT sensor thread, receiving requests and sending measurement responses.
702 ///
703 /// This function runs indefinitely, acting as a dedicated service for reading ambient
704 /// temperature and humidity from the DHT22 sensor. It waits for incoming `InternalCommand`
705 /// from other modules (like `Ambient` or `SignalHandler`).
706 ///
707 /// When a `RequestSignal` for ambient temperature or humidity is received, it performs
708 /// a sensor reading (using `self.get_data()`) and sends the `Result` back. It handles
709 /// `Quit` commands for graceful shutdown and logs errors/warnings for invalid commands or channel issues.
710 ///
711 /// # Arguments
712 /// * `mutex_dht` - Mutex to store the result of the measurement
713 /// * `dht_channels` - Mutable reference to the struct containing the channel
714 #[allow(unused)] // used in conditionally compiled code
715 pub fn execute(&mut self, mutex_dht: Arc<Mutex<DhtResult>>, dht_channels: &mut DhtChannels) {
716 #[cfg(all(target_os = "linux", not(test)))]
717 info!(target: module_path!(), "Thread started with TID: {}", gettid());
718
719 let max_mutex_access_duration =
720 Duration::from_millis(dht22_constants::MAX_MUTEX_ACCESS_DURATION_MILLIS);
721 let mut lock_error_invalid_command: bool = false;
722 let mut lock_error_receive: bool = false;
723 let spin_sleeper = SpinSleeper::default();
724 let sleep_duration_100_millis = Duration::from_millis(100);
725
726 loop {
727 let (
728 quit_command_received, // the request to end the application has been received
729 _,
730 _,
731 ) = self.process_external_request(&mut dht_channels.rx_dht_from_signal_handler, None);
732
733 if quit_command_received {
734 break;
735 }
736
737 spin_sleeper.sleep(sleep_duration_100_millis);
738
739 // Check if data acquisition is enabled.
740 if self.config.active {
741 // Check if a sufficient amount of time has passed since the last measurement.
742 if self.instant_last_measurement.elapsed() > self.measurement_interval {
743 let result = self.get_data();
744 if let Err(ref dht_error) = result {
745 if matches!(
746 dht_error,
747 DhtError::FailedAfterRetries {
748 location: _,
749 retry_counter: _,
750 source: _
751 }
752 ) {
753 if !self.lock_error_failed_after_retries {
754 // avoid log flooding
755 #[cfg(not(test))]
756 log_error_chain(
757 module_path!(),
758 "Failure to read from DHT sensor",
759 dht_error,
760 );
761 self.lock_error_failed_after_retries = true;
762 }
763 } else {
764 self.lock_error_failed_after_retries = false;
765 // report directly
766 #[cfg(not(test))]
767 log_error_chain(
768 module_path!(),
769 "Failure to read from DHT sensor",
770 dht_error,
771 );
772 }
773 }
774
775 let instant_before_locking_mutex = Instant::now();
776 let mut instant_after_locking_mutex = Instant::now(); // initialization is overwritten
777
778 {
779 // internal scope to limit lifetime of mutex lock
780 match mutex_dht.lock() {
781 Ok(mut c) => {
782 instant_after_locking_mutex = Instant::now();
783 *c = result;
784 self.lock_error_mutex = false;
785 }
786 Err(e) => {
787 if !self.lock_error_mutex {
788 self.lock_error_mutex = true;
789 error!(target: module_path!(), "error locking mutex for dht result ({e:?})");
790 }
791 }
792 };
793 }
794
795 // check if access to mutex took too long
796 self.check_mutex_access_duration(
797 None,
798 instant_after_locking_mutex,
799 instant_before_locking_mutex,
800 );
801
802 self.instant_last_measurement = Instant::now();
803 }
804 }
805 }
806 }
807}
808
809#[cfg(test)]
810pub mod tests {
811 use crate::launch::channels::{channel, AquaSender};
812 use crate::sensors::dht::DhtResult;
813 use crate::sensors::dht::{dht22_constants, Dht, DhtError};
814 use crate::sensors::dht_channels::DhtChannels;
815 use crate::sensors::dht_config::DhtConfig;
816 use crate::utilities::channel_content::InternalCommand;
817 use crate::utilities::logger::setup_logger;
818 use crate::utilities::logger_config::LoggerConfig;
819 use assert_float_eq::*;
820 use std::sync::{Arc, Mutex};
821 use std::thread;
822 use std::time::Duration;
823
824 // feature-specific imports
825 cfg_if::cfg_if! {
826 if #[cfg(all(feature = "target_hw", target_os = "linux"))] {
827 use rppal::gpio::Gpio;
828 use crate::utilities::config::{read_config_file, ConfigData};
829 }
830 }
831
832 #[test]
833 // Test case checks if the implementation correctly processes the bit information into an array.
834 // The test case does not require any communication with SQL database.
835 pub fn test_dht_calc_byte_array() {
836 let stimuli: [bool; 40] = [
837 false, // initial interrupt - to be ignored
838 false, false, false, false, false, false, true, false, // Byte 0
839 true, true, false, false, true, true, false, true, // Byte 1
840 false, false, false, false, false, false, false, // Byte 2-7 bits only
841 true, true, true, true, false, true, false, true, // Byte 3
842 true, true, false, false, false, true, false, false, // Byte 4
843 ];
844 for i in 0..40 {
845 println!("stimuli[{}]={}", i, stimuli[i]);
846 }
847
848 let reference: [u8; dht22_constants::DHT22_DATA_LEN] = [2, 205, 0, 245, 196];
849
850 let result = Dht::calc_byte_array(stimuli);
851
852 for i in 0..dht22_constants::DHT22_DATA_LEN {
853 println!(
854 "reference[{}]={} result[{}]={}",
855 i, reference[i], i, result[i]
856 );
857 }
858
859 assert!(reference.iter().eq(result.iter()));
860 }
861
862 #[test]
863 // Test case checks if the implementation correctly calculates a valid checksum.
864 // Test case does not require any communication with the database.
865 pub fn test_dht_calc_checksum_valid() {
866 let mut try_again: bool = false;
867
868 setup_logger(LoggerConfig::default()).unwrap();
869
870 let stimuli: [u8; dht22_constants::DHT22_DATA_LEN] = [2, 205, 0, 245, 196];
871
872 let result = Dht::calc_checksum(stimuli, &mut try_again);
873
874 let ok_result = result.unwrap();
875
876 assert_eq!(ok_result, 196);
877
878 assert!(!try_again);
879 }
880
881 #[test]
882 // Test case checks if the implementation correctly calculates an invalid checksum.
883 // Test case does not require any communication with the database.
884 pub fn test_dht_calc_checksum_error() {
885 let mut try_again: bool = false;
886
887 setup_logger(LoggerConfig::default()).unwrap();
888
889 let stimuli: [u8; dht22_constants::DHT22_DATA_LEN] = [255, 255, 255, 255, 10];
890
891 let result = Dht::calc_checksum(stimuli, &mut try_again);
892
893 assert!(matches!(result, Err(DhtError::ChecksumError(_, _, _))));
894 assert!(try_again);
895 }
896
897 #[test]
898 // Test case checks if the implementation correctly calculates the signals from the byte array
899 // given valid input data.
900 // Test case does not require any communication with the database.
901 pub fn test_dht_calc_signals_valid() {
902 setup_logger(LoggerConfig::default()).unwrap();
903
904 let stimuli: [u8; dht22_constants::DHT22_DATA_LEN] = [2, 205, 0, 245, 196];
905
906 let (temperature, humidity) = match Dht::calc_signals(stimuli) {
907 Ok((c, d)) => (c, d),
908 Err(e) => {
909 panic!("{e:?}");
910 }
911 };
912
913 println!("temperature={}, humidity={}", temperature, humidity);
914 assert_float_absolute_eq!(temperature, 24.5, 0.001);
915 assert_float_absolute_eq!(humidity, 71.7, 0.001);
916 }
917
918 #[test]
919 // Test case checks if the implementation correctly calculates the signals from the byte array
920 // given invalid input data for the humidity.
921 // Test case does not require any communication with the database.
922 pub fn test_dht_calc_signals_humidity_too_high() {
923 setup_logger(LoggerConfig::default()).unwrap();
924
925 let stimuli: [u8; dht22_constants::DHT22_DATA_LEN] = [4, 205, 0, 245, 198];
926
927 assert!(matches!(
928 Dht::calc_signals(stimuli),
929 Err(DhtError::HumidityOutOfRange(_, _, _, _))
930 ));
931 }
932
933 #[test]
934 // Test case checks if the implementation correctly calculates the signals from the byte array
935 // given invalid input data for the temperature.
936 // Test case does not require any communication with the database.
937 pub fn test_dht_calc_signals_temperature_too_high() {
938 setup_logger(LoggerConfig::default()).unwrap();
939
940 let stimuli: [u8; dht22_constants::DHT22_DATA_LEN] = [2, 205, 1, 245, 197];
941
942 assert!(matches!(
943 Dht::calc_signals(stimuli),
944 Err(DhtError::TemperatureOutOfRange(_, _, _, _))
945 ));
946 }
947
948 #[cfg(all(feature = "target_hw", target_os = "linux"))]
949 #[test]
950 // Test case executes a read operation from real hardware.
951 // Test case does not require any communication with the database.
952 pub fn test_dht_read() {
953 setup_logger(LoggerConfig::default()).unwrap();
954
955 let config: ConfigData =
956 read_config_file("/config/aquarium_control_test_generic.toml".to_string());
957
958 // open interface to the GPIO library
959 let gpio = match Gpio::new() {
960 Ok(c) => c,
961 Err(e) => {
962 panic!("test_dht_read: could not open interface to GPIO ({e:?})",);
963 }
964 };
965
966 let gpio_pin_number_dht22_io = config.gpio_handler.dht22_io;
967 let gpio_pin_number_dht22_vcc = config.gpio_handler.dht22_vcc;
968
969 let mut dht = Dht::new(
970 config.dht,
971 Some(gpio),
972 gpio_pin_number_dht22_io,
973 gpio_pin_number_dht22_vcc,
974 );
975
976 let (temperature, humidity) = match dht.get_data() {
977 Ok((c, d)) => (c, d),
978 Err(e) => {
979 panic!("{e:?}");
980 }
981 };
982
983 println!("temperature={}, humidity={}", temperature, humidity);
984 }
985
986 #[test]
987 // Test case checks if the implementation correctly calculates the bit array from
988 // a sample set of GPIO pin timings.
989 // Test case does not require any communication with the database.
990 pub fn test_calc_data_bits() {
991 let stimuli: [u64; dht22_constants::DHT22_TRANSMISSION_END_IDX] = [
992 0, 23, 51, 25, 51, 25, 51, 25, 51, 25, 51, 25, 51, 71, 51, 26, 51, 71, 51, 71, 51, 71,
993 51, 25, 51, 71, 51, 25, 51, 71, 51, 27, 51, 25, 51, 25, 51, 25, 51, 25, 51, 25, 51, 25,
994 51, 25, 51, 33, 51, 71, 51, 71, 51, 71, 51, 71, 51, 25, 51, 71, 51, 71, 51, 73, 51, 71,
995 51, 71, 51, 71, 51, 25, 51, 25, 51, 25, 51, 71, 51, 71, 53, 0, 0, 0, 0, 0,
996 ];
997
998 let result = Dht::calc_data_bits(stimuli, 86);
999
1000 let reference: [bool; dht22_constants::DHT22_BYTE4_STOP_IDX + 1] = [
1001 false, false, false, false, false, false, false, true, false, true, true, true, false,
1002 true, false, true, false, false, false, false, false, false, false, false, true, true,
1003 true, true, false, true, true, true, true, true, true, false, false, false, true, true,
1004 ];
1005
1006 for i in 0..dht22_constants::DHT22_BYTE4_STOP_IDX {
1007 println!(
1008 "reference[{}]={} result[{}]={}",
1009 i, reference[i], i, result[i]
1010 );
1011 }
1012
1013 assert!(reference.iter().eq(result.iter()));
1014 }
1015
1016 /// Helper function to create a standard setup for execute function tests.
1017 /// This reduces boilerplate code across multiple tests.
1018 fn setup_execute_test() -> (
1019 Dht,
1020 DhtChannels,
1021 AquaSender<InternalCommand>,
1022 Arc<Mutex<DhtResult>>,
1023 ) {
1024 let (tx, rx) = channel(1);
1025 let channels = DhtChannels {
1026 rx_dht_from_signal_handler: rx,
1027 #[cfg(feature = "debug_channels")]
1028 cnt_rx_dht_from_signal_handler: 0,
1029 };
1030 // Create a default config for testing purposes.
1031 let config = DhtConfig {
1032 active: false,
1033 measurement_interval_millis: 2000,
1034 sensor_reset_duration_millis: 500,
1035 sensor_startup_duration_millis: 1000,
1036 sensor_pause_duration_millis: 2000,
1037 sensor_init_pin_down_duration_micros: 1000,
1038 sensor_init_pin_up_duration_micros: 40,
1039 sensor_init_input_duration_micros: 0,
1040 timeout_duration_micros: 200,
1041 };
1042 let dht = Dht::new(config, None, 0, 0);
1043 let mutex = Arc::new(Mutex::new(Ok((0.0, 0.0))));
1044
1045 (dht, channels, tx, mutex)
1046 }
1047
1048 #[test]
1049 /// Verifies that the `execute` loop terminates gracefully when a `Quit` command is received.
1050 fn test_execute_handles_quit_command() {
1051 // Arrange
1052 let (mut dht, mut channels, mut tx, mutex) = setup_execute_test();
1053
1054 // Action
1055 let handle = thread::spawn(move || {
1056 dht.execute(mutex, &mut channels);
1057 });
1058 tx.send(InternalCommand::Quit).unwrap();
1059
1060 // Assert
1061 // The thread should join successfully, which proves it terminated as expected.
1062 let join_result = handle.join();
1063 assert!(
1064 join_result.is_ok(),
1065 "The DHT thread should terminate gracefully on Quit."
1066 );
1067 }
1068
1069 #[test]
1070 /// Verifies that the `execute` loop continues running after receiving an unrecognized command.
1071 fn test_execute_handles_invalid_command() {
1072 // Arrange
1073 let (mut dht, mut channels, mut tx, mutex) = setup_execute_test();
1074
1075 // Action
1076 let handle = thread::spawn(move || {
1077 dht.execute(mutex, &mut channels);
1078 });
1079 // Send a command that the `execute` function doesn't explicitly handle.
1080 tx.send(InternalCommand::Stop).unwrap();
1081 // Give the thread a moment to process the invalid command.
1082 thread::sleep(Duration::from_millis(150));
1083 // Now send the Quit command to terminate the loop.
1084 tx.send(InternalCommand::Quit).unwrap();
1085
1086 // Assert
1087 // The thread should not have panicked and should join successfully.
1088 let join_result = handle.join();
1089 assert!(
1090 join_result.is_ok(),
1091 "The DHT thread should handle invalid commands and continue until Quit."
1092 );
1093 }
1094
1095 #[test]
1096 #[cfg(not(all(target_os = "linux", feature = "target_hw")))]
1097 /// Verifies that a measurement is performed and the result is written to the shared mutex
1098 /// when the measurement interval elapses. This test is for non-hardware platforms.
1099 fn test_execute_updates_mutex_on_measurement_interval() {
1100 // Arrange
1101 let (mut tx, rx) = channel(1);
1102 let mut channels = DhtChannels {
1103 rx_dht_from_signal_handler: rx,
1104 #[cfg(feature = "debug_channels")]
1105 cnt_rx_dht_from_signal_handler: 0,
1106 };
1107 let config = DhtConfig {
1108 active: true, // Enable measurement
1109 measurement_interval_millis: 50, // Use a short interval for testing
1110 sensor_reset_duration_millis: 500,
1111 sensor_startup_duration_millis: 1000,
1112 sensor_pause_duration_millis: 2000,
1113 sensor_init_pin_down_duration_micros: 1000,
1114 sensor_init_pin_up_duration_micros: 40,
1115 sensor_init_input_duration_micros: 0,
1116 timeout_duration_micros: 200,
1117 };
1118 let mut dht = Dht::new(config, None, 0, 0);
1119 let mutex = Arc::new(Mutex::new(Ok((0.0, 0.0))));
1120 let mutex_clone = mutex.clone();
1121
1122 // Action
1123 let handle = thread::spawn(move || {
1124 dht.execute(mutex_clone, &mut channels);
1125 });
1126
1127 // Wait for longer than the measurement interval to ensure a measurement is attempted.
1128 thread::sleep(Duration::from_millis(200));
1129
1130 println!("Sending quit command to DHT thread");
1131 tx.send(InternalCommand::Quit).unwrap();
1132 handle.join().unwrap();
1133
1134 // Assert
1135 // On non-hardware platforms, `get_data()` returns `Err(DhtError::IncorrectPlatform)`.
1136 // We check that this specific error was written to the mutex.
1137 let final_result = mutex.lock().unwrap();
1138 assert!(
1139 matches!(*final_result, Err(DhtError::IncorrectPlatform(_))),
1140 "Mutex should contain IncorrectPlatform error on non-hardware targets after measurement"
1141 );
1142 }
1143}