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}