aquarium_control/sensors/
ds18b20.rs

1/* Copyright 2025 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*/
9//! Manages temperature reading from DS18B20 1-Wire digital thermometers.
10//!
11//! This module provides the `Ds18b20` struct, which is designed to run as a dedicated
12//! thread. It interfaces with the Linux 1-Wire subsystem via `sysfs` to periodically
13//! read temperature data from one or more configured sensors.
14//!
15//! ## Responsibilities
16//!
17//! 1.  **Configuration Validation**: The `new()` constructor performs rigorous checks on the
18//!     provided configuration, ensuring that all necessary paths are specified and that
19//!     sensor IDs are unique and valid before the thread starts.
20//! 2.  **Periodic Measurement**: The `execute()` loop periodically reads sensor data based
21//!     on the `measurement_pause_duration_millis` setting in the configuration.
22//! 3.  **Robust Reading**: It implements a retry mechanism with a configurable pause to
23//!     handle transient CRC errors or other temporary read failures common on the
24//!     1-Wire bus.
25//! 4.  **State Sharing**: It updates shared `Arc<Mutex<Ds18b20Result>>` containers with
26//!     the latest water and ambient temperature readings, making the data available to
27//!     other parts of the application in a thread-safe manner.
28//! 5.  **Graceful Shutdown**: The `execute()` loop is responsive to a `Quit` command
29//!     received from the main signal handler, allowing the thread to terminate cleanly.
30//! 6.  **Performance Monitoring**: It includes logic to detect and log warnings if
31//!     access to the shared mutexes is blocked for longer than a predefined threshold.
32
33#[cfg(all(not(test), target_os = "linux"))]
34use nix::unistd::gettid;
35use spin_sleep::SpinSleeper;
36use std::sync::{Arc, Mutex};
37use std::time::{Duration, Instant};
38
39use std::fs;
40use std::path::PathBuf;
41use std::thread::sleep;
42
43use crate::sensors::ds18b20_channels::Ds18b20Channels;
44use crate::sensors::ds18b20_config::Ds18b20Config;
45use crate::sensors::ds18b20_error::Ds18b20Error;
46use crate::utilities::check_mutex_access_duration::CheckMutexAccessDurationTrait;
47use crate::utilities::proc_ext_req::ProcessExternalRequestTrait;
48#[cfg(feature = "debug_ds18b20")]
49use log::debug;
50#[cfg(all(not(test), target_os = "linux"))]
51use log::info;
52
53mod ds18b20_constants {
54    /// minimum viable temperature used for health check of sensor signal
55    pub const MIN_TEMPERATURE: f32 = 0.0;
56
57    /// maximum viable temperature used for health check of sensor signal
58    pub const MAX_TEMPERATURE: f32 = 100.0;
59
60    /// allow max. 10 milliseconds for mutex to be blocked by any other thread
61    pub const MAX_MUTEX_ACCESS_DURATION_MILLIS: u64 = 10;
62}
63
64#[derive(Clone)]
65pub struct Ds18b20ResultData {
66    pub value: f32,
67
68    #[allow(unused)]
69    invalid: bool,
70
71    #[allow(unused)]
72    measurement_instant: Instant,
73}
74
75impl Ds18b20ResultData {
76    pub fn new(value: f32) -> Self {
77        Ds18b20ResultData {
78            value,
79            invalid: false,
80            measurement_instant: Instant::now(),
81        }
82    }
83}
84pub type Ds18b20Result = Result<Ds18b20ResultData, Ds18b20Error>;
85
86/// Represents a Ds18b20 sensor module responsible for reading temperature signals.
87///
88/// This struct encapsulates the configuration, interface details, and internal state
89/// required to communicate with the sensor. It handles communication with the operating system.
90/// Retries, and perform post-processing (checksum validation,
91/// signal calculation) before providing the final temperature and humidity values.
92///
93/// Thread Communication:
94/// This module is designed to run in its own dedicated thread, periodically reading sensor data
95/// and making the latest measurements available via a shared `Arc<Mutex<Ds18b20Result>>`.
96/// Other modules (like `SensorManager` or `DataLogger`) can then read from this mutex to get the
97/// current temperature and humidity. It also responds to `Quit` commands for graceful shutdown.
98///
99/// Platform-Specific Behavior:
100/// The sensor reading logic (`read` function) will only work on Linux systems but compile for
101/// any other system as well.
102///
103/// Thread communication of this component is as follows:
104/// ```mermaid
105/// graph LR
106///     ds18b20[Ds18b20] -.-> sensor_manager[SensorManager]
107///     ds18b20[Ds18b20] -.-> data_logger[DataLogger]
108///     signal_handler[SignalHandler] --> ds18b20
109/// ```
110pub struct Ds18b20 {
111    #[allow(unused)]
112    /// configuration for communication with Ds18b20 sensor
113    config: Ds18b20Config,
114
115    /// inhibition flag to avoid flooding the log file with repeated messages about excessive access time to mutex
116    pub lock_warn_max_mutex_access_duration: bool,
117
118    #[cfg(test)]
119    // for testing purposes: record when mutex access time is exceeded without resetting it
120    pub mutex_access_duration_exceeded: bool,
121
122    /// maximum allowed duration of access to mutex
123    pub max_mutex_access_duration: Duration,
124}
125
126impl ProcessExternalRequestTrait for Ds18b20 {}
127
128impl Ds18b20 {
129    /// Creates a new `Ds18b20` instance, validating the provided configuration.
130    ///
131    /// This constructor initializes the DS18B20 sensor module, preparing it to read
132    /// temperatures from 1-Wire devices via the Linux sysfs interface. It performs
133    /// several critical checks on the configuration to ensure that the module can
134    /// operate correctly.
135    ///
136    /// # Arguments
137    /// * `config` - Configuration data for the DS18B20 sensor, including driver paths,
138    ///   sensor IDs, and retry settings.
139    ///
140    /// # Returns
141    /// A `Result` containing a new `Ds18b20` struct on success.
142    ///
143    /// # Errors
144    /// Returns a `Ds18b20Error` variant if the configuration is invalid, preventing
145    /// the module from being created. This allows the application to handle setup
146    /// failures gracefully. Specific errors include:
147    /// - `Ds18b20Error::SystemDriverBasePathEmpty`: If the base path for the 1-Wire
148    ///   driver in the configuration is empty.
149    /// - `Ds18b20Error::SystemDriverSensorPathEmpty`: If the sensor path prefix is
150    ///   not specified.
151    /// - `Ds18b20Error::SystemDriverFileNameEmpty`: If the filename for the sensor
152    ///   data (e.g., `w1_slave`) is not provided.
153    /// - `Ds18b20Error::NoSensorIdProvided`: If the module is configured to be `active`
154    ///   but both the water and ambient temperature sensor IDs are empty.
155    /// - `Ds18b20Error::SensorIdsIdentical`: If the module is `active` and the same
156    ///   ID is assigned to both the water and ambient temperature sensors.
157    pub fn new(config: Ds18b20Config) -> Result<Ds18b20, Ds18b20Error> {
158        if config.system_driver_base_path.is_empty() {
159            return Err(Ds18b20Error::SystemDriverBasePathEmpty(
160                module_path!().to_string(),
161            ));
162        }
163        if config.system_driver_sensor_path_prefix.is_empty() {
164            return Err(Ds18b20Error::SystemDriverSensorPathEmpty(
165                module_path!().to_string(),
166            ));
167        }
168        if config.system_driver_file_name.is_empty() {
169            return Err(Ds18b20Error::SystemDriverFileNameEmpty(
170                module_path!().to_string(),
171            ));
172        }
173        if config.active
174            && config.water_temperature_sensor_id.is_empty()
175            && config.ambient_temperature_sensor_id.is_empty()
176        {
177            return Err(Ds18b20Error::NoSensorIdProvided(module_path!().to_string()));
178        }
179        if config.active
180            && config.water_temperature_sensor_id == config.ambient_temperature_sensor_id
181        {
182            return Err(Ds18b20Error::SensorIdsIdentical(module_path!().to_string()));
183        }
184        Ok(Ds18b20 {
185            config,
186            lock_warn_max_mutex_access_duration: false,
187            #[cfg(test)]
188            mutex_access_duration_exceeded: false,
189            max_mutex_access_duration: Duration::from_millis(
190                ds18b20_constants::MAX_MUTEX_ACCESS_DURATION_MILLIS,
191            ),
192        })
193    }
194
195    #[allow(unused)]
196    /// Reads temperature from a DS18B20 1-Wire sensor via the Linux sysfs interface.
197    ///
198    /// This function attempts to read temperature data from a specific DS18B20 sensor,
199    /// identified by its `sensor_id`. It constructs the file path based on the configured
200    /// system driver paths. The function includes a retry mechanism for robust reading,
201    /// as 1-Wire communication can sometimes be flaky.
202    ///
203    /// # Arguments
204    /// * `sensor_id` - A string slice containing the unique 64-bit ID of the DS18B20 sensor
205    ///   to read (e.g., "28-00000abcde12").
206    ///
207    /// # Returns
208    /// A `Ds18b20Result` containing `Ds18b20ResultData` on success, which includes the
209    /// temperature in degrees Celsius.
210    ///
211    /// # Errors
212    /// Returns a `Ds18b20Error` variant if the reading fails, even after retries.
213    /// Specific errors include:
214    /// - `Ds18b20Error::SensorIdNotFound`: If the directory for the given `sensor_id` does not exist.
215    /// - `Ds18b20Error::ReadToStringFailure`: If the underlying file read from the sysfs fails.
216    /// - `Ds18b20Error::InsufficientLinesInFile`: If the sensor file does not contain at least two lines.
217    /// - `Ds18b20Error::Line0NotEndingWithYes`: If the first line of the sensor file does not end with "YES",
218    ///   indicating a CRC error or invalid reading.
219    /// - `Ds18b20Error::TemperatureEntryNotFound`: If the second line does not contain the "t=" marker.
220    /// - `Ds18b20Error::TemperatureParseError`: If the temperature value after "t=" cannot be parsed into a number.
221    /// - `Ds18b20Error::TemperatureOutOfRange`: If the parsed temperature is outside the plausible range defined
222    ///   by `MIN_TEMPERATURE` and `MAX_TEMPERATURE`.
223    pub fn read(&self, sensor_id: &str) -> Ds18b20Result {
224        #[cfg(test)]
225        println!("{}: reading from {}", module_path!(), sensor_id);
226
227        let base_dir = PathBuf::from(self.config.system_driver_base_path.clone());
228        let device_folder =
229            base_dir.join(self.config.system_driver_sensor_path_prefix.to_string() + sensor_id);
230
231        if !device_folder.exists() {
232            return Err(Ds18b20Error::SensorIdNotFound(
233                module_path!().to_string(),
234                sensor_id.to_string(),
235            ));
236        }
237
238        let device_file = device_folder.join(self.config.system_driver_file_name.clone());
239        let mut last_error = Ds18b20Error::UnidentifiedError(module_path!().to_string());
240
241        // Use a `for` loop for a cleaner retry mechanism.
242        for _ in 0..=self.config.max_retries {
243            match fs::read_to_string(&device_file) {
244                Ok(contents) => {
245                    // Call the new helper function to handle parsing.
246                    match self.parse_contents(&contents) {
247                        Ok(celsius) => return Ok(Ds18b20ResultData::new(celsius)),
248                        Err(e) => last_error = e, // Store the error and retry.
249                    }
250                }
251                Err(_) => {
252                    last_error = Ds18b20Error::ReadToStringFailure(module_path!().to_string());
253                }
254            }
255            sleep(Duration::from_millis(
256                self.config.retry_pause_duration_millis,
257            ));
258        }
259
260        // If all retries fail, return the last error encountered.
261        Err(last_error)
262    }
263
264    /// Parses the string contents of a DS18B20 sysfs file.
265    /// This private helper contains the core parsing logic.
266    fn parse_contents(&self, contents: &str) -> Result<f32, Ds18b20Error> {
267        let lines: Vec<&str> = contents.lines().collect();
268
269        if lines.len() < 2 {
270            return Err(Ds18b20Error::InsufficientLinesInFile(
271                module_path!().to_string(),
272            ));
273        }
274
275        if !lines[0].ends_with(" YES") {
276            return Err(Ds18b20Error::Line0NotEndingWithYes(
277                module_path!().to_string(),
278            ));
279        }
280
281        let temp_line = lines[1];
282        if let Some(temp_index) = temp_line.find("t=") {
283            let temp_str = temp_line[temp_index + 2..].trim();
284            // Use `map_err` for more concise error conversion.
285            let temp_val = temp_str
286                .parse::<f32>()
287                .map_err(|_| Ds18b20Error::TemperatureParseError(module_path!().to_string()))?;
288
289            let celsius = temp_val / 1000.0;
290            if (ds18b20_constants::MIN_TEMPERATURE..=ds18b20_constants::MAX_TEMPERATURE)
291                .contains(&celsius)
292            {
293                Ok(celsius)
294            } else {
295                Err(Ds18b20Error::TemperatureOutOfRange(
296                    module_path!().to_string(),
297                ))
298            }
299        } else {
300            Err(Ds18b20Error::TemperatureEntryNotFound(
301                module_path!().to_string(),
302            ))
303        }
304    }
305
306    /// Executes the main control loop for the DS18B20 sensor module.
307    ///
308    /// This function runs continuously, managing the periodic measurement of water and ambient
309    /// temperatures from DS18B20 sensors via the Linux sysfs 1-Wire interface.
310    /// It updates shared mutexes with the latest sensor readings, making them available to other
311    /// application threads. The loop remains responsive to `Quit` commands from the
312    /// signal handler for graceful shutdown.
313    ///
314    /// # Arguments
315    /// * `rx_from_signal_handler` - The receiver channel for commands (e.g., `Quit`) from the main signal handler.
316    /// * `mutex_water_temperature` - An `Arc<Mutex<Ds18b20Result>>` into which the latest water temperature reading
317    ///   will be written, ensuring thread-safe access for other modules.
318    /// * `mutex_ambient_temperature` - An `Arc<Mutex<Ds18b20Result>>` into which the latest ambient temperature reading
319    ///   will be written, ensuring thread-safe access for other modules.
320    pub fn execute(
321        &mut self,
322        ds18b20_channels: &mut Ds18b20Channels,
323        mutex_water_temperature: Arc<Mutex<Ds18b20Result>>,
324        mutex_ambient_temperature: Arc<Mutex<Ds18b20Result>>,
325    ) {
326        #[cfg(all(target_os = "linux", not(test)))]
327        info!(target: module_path!(), "Thread started with TID: {}", gettid());
328
329        let sleep_duration_main_cycle = Duration::from_millis(100);
330        let spin_sleeper = SpinSleeper::default();
331        let measurement_interval =
332            Duration::from_millis(self.config.measurement_pause_duration_millis);
333
334        // Use an Instant to track time since the last measurement.
335        // Start with a measurement due to ensure the first loop iteration triggers a read.
336        let mut last_measurement_instant = Instant::now() - measurement_interval;
337
338        loop {
339            // Check for quit command first for faster shutdown.
340            if self
341                .process_external_request(
342                    &mut ds18b20_channels.rx_ds18b20_from_signal_handler,
343                    None,
344                )
345                .0
346            {
347                break;
348            }
349
350            if self.config.active && (last_measurement_instant.elapsed() >= measurement_interval) {
351                let water_temp_result = if !self.config.water_temperature_sensor_id.is_empty() {
352                    Some(self.read(self.config.water_temperature_sensor_id.as_str()))
353                } else {
354                    None
355                };
356
357                let ambient_temp_result = if !self.config.ambient_temperature_sensor_id.is_empty() {
358                    Some(self.read(self.config.ambient_temperature_sensor_id.as_str()))
359                } else {
360                    None
361                };
362
363                // Now, lock the mutexes for the shortest possible time to write the results.
364                let instant_before_locking_mutex = Instant::now();
365
366                if let Some(result) = water_temp_result {
367                    // The lock is held only for the duration of this assignment.
368                    *mutex_water_temperature.lock().unwrap() = result;
369                }
370
371                if let Some(result) = ambient_temp_result {
372                    *mutex_ambient_temperature.lock().unwrap() = result;
373                }
374
375                let instant_after_locking_mutex = Instant::now();
376                last_measurement_instant = Instant::now(); // Reset the timer
377
378                // check if access to mutex took too long
379                self.check_mutex_access_duration(
380                    None,
381                    instant_after_locking_mutex,
382                    instant_before_locking_mutex,
383                );
384            }
385
386            // sleep for a defined period
387            spin_sleeper.sleep(sleep_duration_main_cycle);
388        }
389    }
390}
391
392#[cfg(test)]
393pub mod tests {
394    use crate::launch::channels::{channel, AquaReceiver, AquaSender, Channels};
395    use crate::sensors::ds18b20::{Ds18b20, Ds18b20Config, Ds18b20Error};
396    use crate::sensors::ds18b20::{Ds18b20Result, Ds18b20ResultData};
397    use crate::sensors::ds18b20_channels::Ds18b20Channels;
398    use crate::utilities::channel_content::InternalCommand;
399    use crate::utilities::config::read_config_file;
400    use spin_sleep::SpinSleeper;
401    use std::sync::{Arc, Mutex};
402    use std::time::Duration;
403    use std::{env, thread};
404
405    #[test]
406    #[ignore]
407    #[cfg(all(target_os = "linux", feature = "target_hw"))]
408    // This test case reads from a real DS18B20 sensor.
409    // For this test case to succeed, the DS18B20 sensor needs to be connected,
410    // and the 1-wire interface needs to be enabled (see rasp-config)
411    fn test_read_ds18b20() {
412        let config = read_config_file("config/aquarium_control_test_generic.toml".to_string());
413
414        let water_temperature_sensor_id = config.ds18b20.water_temperature_sensor_id.clone();
415
416        let ds18b20 = Ds18b20::new(config.ds18b20);
417
418        match ds18b20.read(&water_temperature_sensor_id) {
419            Ok(temperature) => {
420                println!("Temperature: {:.2}°C ", temperature.value);
421                assert!(true);
422            }
423            Err(e) => {
424                println!("Error reading DS18B20: {}", e);
425                assert!(false);
426            }
427        }
428    }
429
430    #[test]
431    // This test case read sensor data provided by a file.
432    fn test_read_ds18b20_from_file() {
433        let mut config =
434            read_config_file("config/aquarium_control_test_generic.toml".to_string()).unwrap();
435
436        let out_dir = env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| {
437            panic!("Could not read environment variable CARGO_MANIFEST_DIR.");
438        });
439
440        config.ds18b20.system_driver_base_path = out_dir + "/tests/fixtures";
441
442        let water_temperature_sensor_id = config.ds18b20.water_temperature_sensor_id.clone();
443
444        let ds18b20 = Ds18b20::new(config.ds18b20).unwrap();
445        match ds18b20.read(&water_temperature_sensor_id) {
446            Ok(temperature) => {
447                println!(
448                    "{}: Temperature read from file: {:.2}°C ({} milliseconds ago)",
449                    module_path!(),
450                    temperature.value,
451                    temperature.measurement_instant.elapsed().as_millis()
452                );
453                assert!(true);
454            }
455            Err(e) => {
456                println!("Error reading DS18B20: {}", e);
457                assert!(false);
458            }
459        }
460    }
461
462    #[test]
463    // Test case checks if invalid configuration of the system driver base path is recognized during startup.
464    fn test_dsb18b20_init_with_invalid_system_driver_base_path() {
465        let mut config =
466            read_config_file("config/aquarium_control_test_generic.toml".to_string()).unwrap();
467        config.ds18b20.system_driver_base_path = "".to_string();
468        let result = Ds18b20::new(config.ds18b20);
469        assert!(matches!(
470            result,
471            Err(Ds18b20Error::SystemDriverBasePathEmpty(_))
472        ));
473    }
474
475    #[test]
476    // Test case checks if invalid configuration of the system driver sensor path prefix is recognized during startup.
477    fn test_dsb18b20_init_with_invalid_system_driver_sensor_path_prefix() {
478        let mut config =
479            read_config_file("config/aquarium_control_test_generic.toml".to_string()).unwrap();
480        config.ds18b20.system_driver_sensor_path_prefix = "".to_string();
481        let result = Ds18b20::new(config.ds18b20);
482        assert!(matches!(
483            result,
484            Err(Ds18b20Error::SystemDriverSensorPathEmpty(_))
485        ));
486    }
487
488    #[test]
489    // Test case checks if invalid configuration of the system driver sensor file name is recognized during startup.
490    fn test_dsb18b20_init_with_invalid_system_driver_file_name() {
491        let mut config =
492            read_config_file("config/aquarium_control_test_generic.toml".to_string()).unwrap();
493        config.ds18b20.system_driver_file_name = "".to_string();
494        let result = Ds18b20::new(config.ds18b20);
495        assert!(matches!(
496            result,
497            Err(Ds18b20Error::SystemDriverFileNameEmpty(_))
498        ));
499    }
500    #[test]
501    fn test_new_fails_with_identical_sensor_ids() {
502        // Arrange: Create a configuration where both water and ambient sensors
503        // are assigned the exact same ID.
504        let identical_id = "28-00000abcdef12".to_string();
505        let config = Ds18b20Config {
506            active: true,
507            water_temperature_sensor_id: identical_id.clone(),
508            ambient_temperature_sensor_id: identical_id,
509            // Provide valid paths to pass other validation checks.
510            system_driver_base_path: "/sys/bus/w1/devices".to_string(),
511            system_driver_sensor_path_prefix: "28-".to_string(),
512            system_driver_file_name: "w1_slave".to_string(),
513            max_retries: 3,
514            retry_pause_duration_millis: 100,
515            measurement_pause_duration_millis: 1000,
516            execute: true,
517        };
518
519        // Act: Attempt to create a new Ds18b20 instance with the invalid config.
520        let result = Ds18b20::new(config);
521
522        // Assert: Verify that the result is an error and that the error variant
523        // is specifically `SensorIdsIdentical`.
524        assert!(matches!(result, Err(Ds18b20Error::SensorIdsIdentical(_))));
525    }
526
527    #[test]
528    // Test case checks if invalid configuration of the sensor ids is recognized during startup.
529    fn test_dsb18b20_init_with_invalid_sensor_ids() {
530        let mut config =
531            read_config_file("config/aquarium_control_test_generic.toml".to_string()).unwrap();
532        config.ds18b20.active = true;
533        config.ds18b20.water_temperature_sensor_id = "".to_string();
534        config.ds18b20.ambient_temperature_sensor_id = "".to_string();
535        let result = Ds18b20::new(config.ds18b20);
536        assert!(matches!(result, Err(Ds18b20Error::NoSensorIdProvided(_))));
537    }
538
539    #[test]
540    // Test case checks if function returns error for an incorrect sensor id
541    fn test_dsb18b20_read_from_invalid_sensor_id() {
542        let mut config =
543            read_config_file("config/aquarium_control_test_generic.toml".to_string()).unwrap();
544
545        let out_dir = env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| {
546            panic!("Could not read environment variable CARGO_MANIFEST_DIR.");
547        });
548
549        config.ds18b20.system_driver_base_path = out_dir + "/tests/fixtures";
550
551        let invalid_temperature_sensor_id = "33";
552
553        let ds18b20 = Ds18b20::new(config.ds18b20).unwrap();
554
555        assert_eq!(
556            matches!(
557                ds18b20.read(&invalid_temperature_sensor_id),
558                Err(Ds18b20Error::SensorIdNotFound(
559                    _,
560                    _invalid_temperature_sensor_id
561                ))
562            ),
563            true
564        );
565    }
566
567    #[test]
568    // Test case checks if function returns error when not being able to read from the device file
569    fn test_dsb18b20_read_from_file_failed() {
570        let mut config =
571            read_config_file("config/aquarium_control_test_generic.toml".to_string()).unwrap();
572
573        config.ds18b20.system_driver_file_name = "xxx".to_string(); // invalidate file name
574        let out_dir = env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| {
575            panic!("Could not read environment variable CARGO_MANIFEST_DIR.");
576        });
577        config.ds18b20.system_driver_base_path = out_dir + "/tests/fixtures";
578
579        let valid_temperature_sensor_id = config.ds18b20.water_temperature_sensor_id.clone();
580
581        let ds18b20 = Ds18b20::new(config.ds18b20).unwrap();
582
583        assert_eq!(
584            matches!(
585                ds18b20.read(&valid_temperature_sensor_id),
586                Err(Ds18b20Error::ReadToStringFailure(_))
587            ),
588            true
589        );
590    }
591
592    #[test]
593    // Test case checks if function returns error when the device file does not provide enough lines
594    fn test_dsb18b20_file_with_one_line_only() {
595        let mut config =
596            read_config_file("config/aquarium_control_test_generic.toml".to_string()).unwrap();
597
598        config.ds18b20.system_driver_file_name += "_one_line"; // file with only one line content
599        let out_dir = env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| {
600            panic!("Could not read environment variable CARGO_MANIFEST_DIR.");
601        });
602        config.ds18b20.system_driver_base_path = out_dir + "/tests/fixtures";
603
604        let valid_temperature_sensor_id = config.ds18b20.water_temperature_sensor_id.clone();
605
606        let ds18b20 = Ds18b20::new(config.ds18b20).unwrap();
607
608        assert_eq!(
609            matches!(
610                ds18b20.read(&valid_temperature_sensor_id),
611                Err(Ds18b20Error::InsufficientLinesInFile(_))
612            ),
613            true
614        );
615    }
616
617    #[test]
618    // Test case checks if function returns error when the first line of device file content does not match "YES"
619    fn test_dsb18b20_file_with_line_not_ending_with_yes() {
620        let mut config =
621            read_config_file("config/aquarium_control_test_generic.toml".to_string()).unwrap();
622
623        config.ds18b20.system_driver_file_name += "_no_yes"; // file with only one line content
624        let out_dir = env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| {
625            panic!("Could not read environment variable CARGO_MANIFEST_DIR.");
626        });
627        config.ds18b20.system_driver_base_path = out_dir + "/tests/fixtures";
628
629        let valid_temperature_sensor_id = config.ds18b20.water_temperature_sensor_id.clone();
630
631        let ds18b20 = Ds18b20::new(config.ds18b20).unwrap();
632
633        assert_eq!(
634            matches!(
635                ds18b20.read(&valid_temperature_sensor_id),
636                Err(Ds18b20Error::Line0NotEndingWithYes(_))
637            ),
638            true
639        );
640    }
641
642    #[test]
643    // Test case checks if function returns error when file content does not contain temperature
644    fn test_dsb18b20_file_without_temperature() {
645        let mut config =
646            read_config_file("config/aquarium_control_test_generic.toml".to_string()).unwrap();
647
648        config.ds18b20.system_driver_file_name += "_no_temperature"; // file with only one line content
649        let out_dir = env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| {
650            panic!("Could not read environment variable CARGO_MANIFEST_DIR.");
651        });
652        config.ds18b20.system_driver_base_path = out_dir + "/tests/fixtures";
653
654        let valid_temperature_sensor_id = config.ds18b20.water_temperature_sensor_id.clone();
655
656        let ds18b20 = Ds18b20::new(config.ds18b20).unwrap();
657
658        assert_eq!(
659            matches!(
660                ds18b20.read(&valid_temperature_sensor_id),
661                Err(Ds18b20Error::TemperatureEntryNotFound(_))
662            ),
663            true
664        );
665    }
666
667    #[test]
668    // Test case checks if function returns error when file content contains unparsable data
669    fn test_dsb18b20_unparsable() {
670        let mut config =
671            read_config_file("config/aquarium_control_test_generic.toml".to_string()).unwrap();
672
673        config.ds18b20.system_driver_file_name += "_unparsable"; // file with only one line content
674        let out_dir = env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| {
675            panic!("Could not read environment variable CARGO_MANIFEST_DIR.");
676        });
677        config.ds18b20.system_driver_base_path = out_dir + "/tests/fixtures";
678
679        let valid_temperature_sensor_id = config.ds18b20.water_temperature_sensor_id.clone();
680
681        let ds18b20 = Ds18b20::new(config.ds18b20).unwrap();
682
683        assert_eq!(
684            matches!(
685                ds18b20.read(&valid_temperature_sensor_id),
686                Err(Ds18b20Error::TemperatureParseError(_))
687            ),
688            true
689        );
690    }
691
692    #[test]
693    // Test case checks if function returns error when the temperature is too hot
694    fn test_dsb18b20_temperature_too_hot() {
695        let mut config =
696            read_config_file("config/aquarium_control_test_generic.toml".to_string()).unwrap();
697
698        config.ds18b20.system_driver_file_name += "_too_hot"; // file with only one line content
699        let out_dir = env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| {
700            panic!("Could not read environment variable CARGO_MANIFEST_DIR.");
701        });
702        config.ds18b20.system_driver_base_path = out_dir + "/tests/fixtures";
703
704        let valid_temperature_sensor_id = config.ds18b20.water_temperature_sensor_id.clone();
705
706        let ds18b20 = Ds18b20::new(config.ds18b20).unwrap();
707
708        assert_eq!(
709            matches!(
710                ds18b20.read(&valid_temperature_sensor_id),
711                Err(Ds18b20Error::TemperatureOutOfRange(_))
712            ),
713            true
714        );
715    }
716
717    #[test]
718    // Test case checks if function returns error when the temperature is too cold
719    fn test_dsb18b20_temperature_too_cold() {
720        let mut config =
721            read_config_file("config/aquarium_control_test_generic.toml".to_string()).unwrap();
722
723        config.ds18b20.system_driver_file_name += "_too_cold"; // file with only one line content
724        let out_dir = env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| {
725            panic!("Could not read environment variable CARGO_MANIFEST_DIR.");
726        });
727        config.ds18b20.system_driver_base_path = out_dir + "/tests/fixtures";
728
729        let valid_temperature_sensor_id = config.ds18b20.water_temperature_sensor_id.clone();
730
731        let ds18b20 = Ds18b20::new(config.ds18b20).unwrap();
732
733        assert_eq!(
734            matches!(
735                ds18b20.read(&valid_temperature_sensor_id),
736                Err(Ds18b20Error::TemperatureOutOfRange(_))
737            ),
738            true
739        );
740    }
741
742    #[test]
743    fn test_dsb18b20_channel_mutexes_without_locking_mutexes() {
744        let mut config =
745            read_config_file("config/aquarium_control_test_generic.toml".to_string()).unwrap();
746
747        let out_dir = env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| {
748            panic!("Could not read environment variable CARGO_MANIFEST_DIR.");
749        });
750
751        config.ds18b20.system_driver_base_path = out_dir + "/tests/fixtures";
752        config.ds18b20.ambient_temperature_sensor_id = "0114540015ab".to_string();
753
754        // Mutexes for DS18B20 signals
755        let mutex_ds18b20_water_temperature: Arc<Mutex<Ds18b20Result>> = Arc::new(Mutex::new(Ok(
756            Ds18b20ResultData::new(config.sensor_manager.replacement_value_water_temperature),
757        )));
758        let mutex_ds18b20_ambient_temperature: Arc<Mutex<Ds18b20Result>> =
759            Arc::new(Mutex::new(Ok(Ds18b20ResultData::new(
760                config.sensor_manager.replacement_value_ambient_temperature,
761            ))));
762        let mutex_ds18b20_water_temperature_clone_for_asserts =
763            mutex_ds18b20_water_temperature.clone();
764        let mutex_ds18b20_ambient_temperature_clone_for_asserts =
765            mutex_ds18b20_ambient_temperature.clone();
766
767        let mut ds18b20 = Ds18b20::new(config.ds18b20).unwrap();
768
769        let mut channels = Channels::new_for_test();
770
771        // thread for test environment (incl. signal handler)
772        let join_handle_test_environment = thread::Builder::new()
773            .name("test_environment".to_string())
774            .spawn(move || {
775                // let test run a bit longer than the measurement interval
776                let sleep_time = Duration::from_millis(1000);
777                let spin_sleeper = SpinSleeper::default();
778                spin_sleeper.sleep(sleep_time);
779                channels
780                    .signal_handler
781                    .send_to_ds18b20(InternalCommand::Quit)
782                    .unwrap();
783            })
784            .unwrap();
785
786        // thread for the test object
787        let join_handle_test_object = thread::Builder::new()
788            .name("test_object".to_string())
789            .spawn(move || {
790                ds18b20.execute(
791                    &mut channels.ds18b20,
792                    mutex_ds18b20_water_temperature,
793                    mutex_ds18b20_ambient_temperature,
794                );
795                assert_eq!(ds18b20.mutex_access_duration_exceeded, false);
796            })
797            .unwrap();
798
799        join_handle_test_object
800            .join()
801            .expect("Test object did not finish.");
802        join_handle_test_environment
803            .join()
804            .expect("Test environment did not finish.");
805
806        match mutex_ds18b20_water_temperature_clone_for_asserts.lock() {
807            Ok(result) => {
808                let result_data = result.clone().unwrap();
809                assert_eq!(result_data.value, 24.437);
810            }
811            Err(e) => {
812                panic!("mutex_ds18b20_water_temperature lock poisoned: {:?}", e);
813            }
814        };
815        match mutex_ds18b20_ambient_temperature_clone_for_asserts.lock() {
816            Ok(result) => {
817                let result_data = result.clone().unwrap();
818                assert_eq!(result_data.value, 24.637);
819            }
820            Err(e) => {
821                panic!("mutex_ds18b20_ambient_temperature lock poisoned: {:?}", e);
822            }
823        };
824    }
825
826    #[test]
827    fn test_dsb18b20_channel_mutexes_with_locking_mutexes() {
828        let mut config =
829            read_config_file("config/aquarium_control_test_generic.toml".to_string()).unwrap();
830
831        let out_dir = env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| {
832            panic!("Could not read environment variable CARGO_MANIFEST_DIR.");
833        });
834
835        config.ds18b20.system_driver_base_path = out_dir + "/tests/fixtures";
836        config.ds18b20.ambient_temperature_sensor_id = "0114540015ab".to_string();
837        config.ds18b20.measurement_pause_duration_millis = 500;
838
839        // Mutexes for DS18B20 signals
840        let mutex_ds18b20_water_temperature: Arc<Mutex<Ds18b20Result>> = Arc::new(Mutex::new(Ok(
841            Ds18b20ResultData::new(config.sensor_manager.replacement_value_water_temperature),
842        )));
843        let mutex_ds18b20_ambient_temperature: Arc<Mutex<Ds18b20Result>> =
844            Arc::new(Mutex::new(Ok(Ds18b20ResultData::new(
845                config.sensor_manager.replacement_value_ambient_temperature,
846            ))));
847        let mutex_ds18b20_water_temperature_clone_for_test_environment =
848            mutex_ds18b20_water_temperature.clone();
849        let mutex_ds18b20_water_temperature_clone_for_asserts =
850            mutex_ds18b20_water_temperature.clone();
851        let mutex_ds18b20_ambient_temperature_clone_for_asserts =
852            mutex_ds18b20_ambient_temperature.clone();
853
854        let mut ds18b20 = Ds18b20::new(config.ds18b20).unwrap();
855
856        // *** signal handler => ds18b20 ***
857        let (mut tx_signal_handler_to_ds18b20, rx_ds18b20_from_signal_handler): (
858            AquaSender<InternalCommand>,
859            AquaReceiver<InternalCommand>,
860        ) = channel(1);
861        let mut ds18b20_channels = Ds18b20Channels {
862            rx_ds18b20_from_signal_handler,
863            #[cfg(feature = "debug_channels")]
864            cnt_rx_ds18b20_from_signal_handler: 0,
865        };
866
867        // thread for test environment (incl. signal handler)
868        let join_handle_test_environment = thread::Builder::new()
869            .name("test_environment".to_string())
870            .spawn(move || {
871                // let test run a bit longer than the measurement interval
872                let sleep_time = Duration::from_millis(2000);
873                let spin_sleeper = SpinSleeper::default();
874
875                {
876                    // lock the mutexes and wait for triggering the detection
877                    match mutex_ds18b20_water_temperature_clone_for_test_environment.lock() {
878                        Ok(_) => {
879                            spin_sleeper.sleep(sleep_time);
880                        }
881                        Err(e) => {
882                            panic!("mutex_ds18b20_water_temperature lock poisoned: {:?}", e);
883                        }
884                    };
885                }
886
887                tx_signal_handler_to_ds18b20
888                    .send(InternalCommand::Quit)
889                    .unwrap();
890                spin_sleeper.sleep(sleep_time);
891            })
892            .unwrap();
893
894        // thread for the test object
895        let join_handle_test_object = thread::Builder::new()
896            .name("test_object".to_string())
897            .spawn(move || {
898                ds18b20.execute(
899                    &mut ds18b20_channels,
900                    mutex_ds18b20_water_temperature,
901                    mutex_ds18b20_ambient_temperature,
902                );
903                assert_eq!(ds18b20.mutex_access_duration_exceeded, true);
904            })
905            .unwrap();
906
907        join_handle_test_object
908            .join()
909            .expect("Test object did not finish.");
910        join_handle_test_environment
911            .join()
912            .expect("Test environment did not finish.");
913
914        match mutex_ds18b20_water_temperature_clone_for_asserts.lock() {
915            Ok(result) => {
916                let result_data = result.clone().unwrap();
917                assert_eq!(result_data.value, 24.437);
918            }
919            Err(e) => {
920                panic!("mutex_ds18b20_water_temperature lock poisoned: {:?}", e);
921            }
922        };
923        match mutex_ds18b20_ambient_temperature_clone_for_asserts.lock() {
924            Ok(result) => {
925                let result_data = result.clone().unwrap();
926                assert_eq!(result_data.value, 24.637);
927            }
928            Err(e) => {
929                panic!("mutex_ds18b20_ambient_temperature lock poisoned: {:?}", e);
930            }
931        };
932    }
933}