aquarium_control/thermal/
ventilation.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*/
9
10use log::error;
11#[cfg(not(test))]
12use log::warn;
13
14#[cfg(not(test))]
15use log::info;
16
17use crate::database::thermal_set_value_updater_trait::ThermalSetValueUpdaterTrait;
18use crate::sensors::sensor_manager::SensorManagerSignals;
19use crate::utilities::channel_content::{ActuatorState, AquariumDevice, InternalCommand};
20use crate::utilities::common::check_if_mutex_is_blocked;
21use crate::utilities::proc_ext_req::ProcessExternalRequestTrait;
22use crate::utilities::sawtooth_profile::SawToothProfile;
23use spin_sleep::SpinSleeper;
24use std::sync::{Arc, Mutex};
25use std::time::{Duration, Instant};
26
27use crate::thermal::ventilation_channels::VentilationChannels;
28use crate::thermal::ventilation_config::VentilationConfig;
29use crate::utilities::acknowledge_signal_handler::AcknowledgeSignalHandlerTrait;
30use crate::utilities::check_mutex_access_duration::CheckMutexAccessDurationTrait;
31use crate::utilities::logger::log_error_chain;
32use crate::utilities::wait_for_termination::WaitForTerminationTrait;
33use crate::{manage_cycle_time_thermal, perform_schedule_check, update_thermal_set_values};
34#[cfg(all(not(test), target_os = "linux"))]
35use nix::unistd::gettid;
36
37const CYCLE_TIME_VENTILATION_MILLIS: u64 = 500;
38
39const TIME_INCREMENT_VENTILATION_SECS: f32 = CYCLE_TIME_VENTILATION_MILLIS as f32 / 1000.0;
40
41/// allow max. 10 milliseconds for mutex to be blocked by any other thread
42const MAX_MUTEX_ACCESS_DURATION_MILLIS: u64 = 10;
43
44/// Sends a command to the relay manager to actuate the ventilation and waits for a response.
45///
46/// This private helper function encapsulates the logic for sending a command
47/// (either `SwitchOn` or `SwitchOff`) to the relay manager. It handles channel
48/// communication, error logging, and updates the shared actuation mutex.
49///
50/// # Arguments
51/// * `command` - The `InternalCommand` to send (`SwitchOn` or `SwitchOff`).
52/// * `mutex_device_scheduler_ventilation` - A reference to the shared actuation mutex.
53/// * `mutex_blocked_during_actuation` - A mutable flag to track if the actuation mutex was blocked.
54/// * `ventilation_channels` - A mutable reference to the struct containing the channels.
55fn actuate_ventilation(
56    command: InternalCommand,
57    mutex_device_scheduler_ventilation: &Arc<Mutex<i32>>,
58    mutex_blocked_during_actuation: &mut bool,
59    ventilation_channels: &mut VentilationChannels,
60) {
61    // reduce the probability of wrong warning
62    *mutex_blocked_during_actuation |=
63        check_if_mutex_is_blocked(mutex_device_scheduler_ventilation);
64
65    // avoid electrical overloads through parallel device actuation
66    let mut mutex_data = mutex_device_scheduler_ventilation.lock().unwrap();
67
68    // Use if let for cleaner error handling
69    if let Err(e) = ventilation_channels.send_to_relay_manager(command.clone()) {
70        let error_message = format!("Channel communication to relay manager for {command} failed.");
71        log_error_chain(module_path!(), &error_message, e);
72    } else {
73        // now listen for the response
74        if let Err(e) = ventilation_channels.receive_from_relay_manager() {
75            let error_message =
76                format!("Receiving answer from relay manager for {command} failed.");
77            log_error_chain(module_path!(), &error_message, e);
78        }
79    };
80    *mutex_data += 1;
81}
82
83/// The function commands the relay manager to switch on the ventilation device.
84///
85/// This function is a specific wrapper around `actuate_ventilation` that sends the `SwitchOn` command.
86///
87/// # Arguments
88/// * `mutex_device_scheduler_ventilation` - A reference to an `Arc<Mutex<i32>>` used for
89///   coordinating device actuation.
90/// * `mutex_blocked_during_actuation` - A mutable boolean flag that is set if the
91/// * `ventilation_channels` - A mutable reference to the struct containing the channels.
92///   Actuation mutex was found to be blocked.
93fn switch_on_ventilation(
94    mutex_device_scheduler_ventilation: &Arc<Mutex<i32>>,
95    mutex_blocked_during_actuation: &mut bool,
96    ventilation_channels: &mut VentilationChannels,
97) {
98    actuate_ventilation(
99        InternalCommand::SwitchOn(AquariumDevice::Ventilation),
100        mutex_device_scheduler_ventilation,
101        mutex_blocked_during_actuation,
102        ventilation_channels,
103    );
104}
105
106/// The function commands the relay manager to switch off the ventilation device.
107///
108/// This function is a specific wrapper around `actuate_ventilation` that sends the `SwitchOff` command.
109///
110/// # Arguments
111/// * `mutex_device_scheduler_ventilation` - A reference to an `Arc<Mutex<i32>>` used for
112///   coordinating device actuation.
113/// * `mutex_blocked_during_actuation` - A mutable boolean flag that is set if the
114/// * `ventilation_channels` - A mutable reference to the struct containing the channels.
115///   Actuation mutex was found to be blocked.
116fn switch_off_ventilation(
117    mutex_device_scheduler_ventilation: &Arc<Mutex<i32>>,
118    mutex_blocked_during_actuation: &mut bool,
119    ventilation_channels: &mut VentilationChannels,
120) {
121    actuate_ventilation(
122        InternalCommand::SwitchOff(AquariumDevice::Ventilation),
123        mutex_device_scheduler_ventilation,
124        mutex_blocked_during_actuation,
125        ventilation_channels,
126    );
127}
128
129#[cfg_attr(doc, aquamarine::aquamarine)]
130/// Holds the configuration and the implementation for the ventilation control.
131/// Thread communication of this component is as follows:
132/// ```mermaid
133/// graph LR
134///     atlas_scientific[Atlas Scientific] --> ventilation
135///     ventilation --> relay_manager[Relay Manager]
136///     relay_manager --> ventilation
137///     ventilation --> signal_handler[Signal Handler]
138///     signal_handler --> ventilation
139///     ventilation --> data_logger[Data Logger]
140///     data_logger --> ventilation
141///     ventilation --> schedule_check[Schedule Check]
142///     schedule_check --> ventilation
143///     messaging[Messaging] --> ventilation
144/// ```
145pub struct Ventilation {
146    config: VentilationConfig,
147
148    /// inhibition flag to avoid flooding the log file with repeated messages to send request via the channel to schedule check
149    lock_error_channel_send_schedule_check: bool,
150
151    /// inhibition flag to avoid flooding the log file with repeated messages to receive request via the channel from schedule check
152    lock_error_channel_receive_schedule_check: bool,
153
154    /// inhibition flag to avoid flooding the log file with repeated messages about excessive access time to mutex
155    pub lock_warn_max_mutex_access_duration: bool,
156
157    /// for testing purposes: record when mutex access time is exceeded without resetting it
158    #[cfg(test)]
159    pub mutex_access_duration_exceeded: bool,
160
161    /// inhibition flag to avoid flooding the log file about failure to update the set values
162    lock_error_ventilation_set_value_read_failure: bool,
163
164    /// inhibition flag to avoid flooding the log file with repeated messages about having received inapplicable command via the channel
165    pub lock_warn_inapplicable_command_signal_handler: bool,
166
167    /// inhibition flag to avoid flooding the log file with repeated messages about the channel being disconnected
168    pub lock_error_channel_receive_termination: bool,
169
170    /// Maximum permissible access duration for Mutex
171    pub max_mutex_access_duration: Duration,
172}
173
174impl ProcessExternalRequestTrait for Ventilation {}
175
176impl Ventilation {
177    /// Creates a new `Ventilation` control instance.
178    ///
179    /// This constructor initializes the ventilation control module with its specific
180    /// configuration. It sets all internal "lock" flags to `false` by default; these
181    /// flags are used during operation to prevent log files from being flooded with
182    /// repeated error or warning messages.
183    ///
184    /// # Arguments
185    /// * `config` - **Configuration data** for the ventilation control, loaded from a TOML file.
186    ///   This includes parameters such as temperatures for switching on/off, and various
187    ///   behavioral strategies.
188    ///
189    /// # Returns
190    /// A new `Ventilation` struct, ready to manage the aquarium's ventilation system.
191    pub fn new(config: VentilationConfig) -> Ventilation {
192        Self {
193            config,
194            lock_error_channel_send_schedule_check: false,
195            lock_error_channel_receive_schedule_check: false,
196            lock_warn_max_mutex_access_duration: false,
197            lock_error_ventilation_set_value_read_failure: false,
198
199            lock_warn_inapplicable_command_signal_handler: false,
200            #[cfg(test)]
201            mutex_access_duration_exceeded: false,
202            lock_error_channel_receive_termination: false,
203            max_mutex_access_duration: Duration::from_millis(MAX_MUTEX_ACCESS_DURATION_MILLIS),
204        }
205    }
206
207    /// Calculates a **normalized control deviation** based on the measured temperature and configured thresholds.
208    ///
209    /// This private helper function determines how far the `measured_value` deviates
210    /// from the `switch_off_temperature` relative to the total temperature control range
211    /// (`switch_on_temperature` to `switch_off_temperature`). The result is a float
212    /// between `0.0` and `1.0`. A value of `0.0` means the temperature is at or below
213    /// `switch_off_temperature`, and `1.0` means it's at or above `switch_on_temperature`.
214    ///
215    /// # Arguments
216    /// * `measured_value` - The current measured temperature (`f32`).
217    ///
218    /// # Returns
219    /// An `f32` representing the normalized control deviation, ranging from `0.0` to `1.0`.
220    /// Returns `0.0` if `switch_on_temperature` is not greater than `switch_off_temperature`
221    /// (i.e., `target_value_delta` is not positive), indicating an invalid configuration.
222    fn calc_normalized_control_deviation(&self, measured_value: f32) -> f32 {
223        let target_value_delta: f32 =
224            self.config.switch_on_temperature - self.config.switch_off_temperature;
225        let measured_value_delta: f32 = measured_value - self.config.switch_off_temperature;
226        if target_value_delta > 0.0 {
227            measured_value_delta / target_value_delta
228        } else {
229            0.0 // default value when target values are not properly set up.
230        }
231    }
232
233    /// Conditionally switches the ventilation device ON or OFF based on a boolean condition.
234    ///
235    /// This private helper function encapsulates the logic for changing the ventilation's
236    /// state. It only triggers a switch if the `ventilation_state` is not already
237    /// in the desired `condition`. It relies on `switch_on_ventilation` and `switch_off_ventilation`
238    /// to interact with the relay manager.
239    ///
240    /// # Arguments
241    /// * `condition` - A boolean value. If `true`, the function attempts to switch the ventilation ON;
242    ///   if `false`, it attempts to switch it OFF.
243    /// * `ventilation_state` - A mutable reference to the `ActuatorState` representing the
244    ///   current state of the ventilation device, which will be updated by this function.
245    /// * `mutex_device_scheduler_ventilation` - A shared mutex for coordinating device actuation.
246    /// * `ventilation_channels` - A reference to the `VentilationChannels` struct for communication with the relay manager.
247    /// * `mutex_was_blocked_during_actuation` - A mutable flag indicating if the device scheduler mutex was blocked.
248    fn conditional_ventilation_switch(
249        condition: bool,
250        ventilation_state: &mut ActuatorState,
251        mutex_device_scheduler_ventilation: &Arc<Mutex<i32>>,
252        ventilation_channels: &mut VentilationChannels,
253        mutex_was_blocked_during_actuation: &mut bool,
254    ) {
255        match condition {
256            true => {
257                // switch on the ventilation if not already on
258                if *ventilation_state != ActuatorState::On {
259                    switch_on_ventilation(
260                        mutex_device_scheduler_ventilation,
261                        mutex_was_blocked_during_actuation,
262                        ventilation_channels,
263                    );
264                    *ventilation_state = ActuatorState::On;
265                }
266            }
267            false => {
268                // switch off the ventilation if not already off
269                if *ventilation_state != ActuatorState::Off {
270                    switch_off_ventilation(
271                        mutex_device_scheduler_ventilation,
272                        mutex_was_blocked_during_actuation,
273                        ventilation_channels,
274                    );
275                    *ventilation_state = ActuatorState::Off;
276                }
277            }
278        }
279    }
280
281    /// Executes the main control loop for the ventilation module.
282    ///
283    /// This function runs continuously, managing the aquarium's ventilation system. It adjusts
284    /// ventilation based on water temperature readings, a calculated sawtooth profile (for
285    /// proportional control), and various external commands and schedule limitations.
286    ///
287    /// The loop periodically reads water temperature, checks schedule permissions, and updates
288    /// the ventilation state via the relay manager. It is responsive to `Start`, `Stop`, and `Quit`
289    /// commands from external channels. The function maintains a fixed cycle time and ensures
290    /// a graceful shutdown upon receiving termination signals.
291    ///
292    /// # Arguments
293    /// * `mutex_device_scheduler_ventilation` - An `Arc<Mutex<i32>>` used for coordinating
294    ///   device actuation, preventing parallel operations, and tracking actuation counts.
295    /// * `ventilation_channels` - A mutable reference to `VentilationChannels` struct containing all `mpsc`
296    ///   channels for communication with other threads (e.g., relay manager, data logger, schedule checker, signal handler).
297    /// * `mutex_sensor_manager_channels` - An `Arc<Mutex<SensorManagerSignals>>` from which the latest water temperature
298    ///   reading will be retrieved.
299    /// * `mutex_ventilation_status` - An `Arc<Mutex<bool>>` for communicating the ventilation status to the data logger.
300    ///
301    /// # Returns
302    /// This function does not return a value in the traditional sense, as it is designed
303    /// to loop indefinitely. It will only break out of its loop and terminate when a `Quit`
304    /// command is received from the signal handler, after which it performs final cleanup
305    /// (switching the ventilation to its configured terminal state) and confirms shutdown.
306    pub fn execute(
307        &mut self,
308        mutex_device_scheduler_ventilation: Arc<Mutex<i32>>,
309        ventilation_channels: &mut VentilationChannels,
310        ventilation_set_val_updater: &mut impl ThermalSetValueUpdaterTrait,
311        mutex_sensor_manager_signals: Arc<Mutex<SensorManagerSignals>>,
312        mutex_ventilation_status: Arc<Mutex<bool>>,
313    ) {
314        #[cfg(all(target_os = "linux", not(test)))]
315        info!(target: module_path!(), "Thread started with TID: {}", gettid());
316
317        let cycle_time_duration = Duration::from_millis(CYCLE_TIME_VENTILATION_MILLIS);
318        let sleep_duration_hundred_millis = Duration::from_millis(100);
319        let spin_sleeper = SpinSleeper::default();
320        let mut saw_tooth_profile = SawToothProfile::new(&self.config.saw_tooth_profile_config);
321        let mut measured_water_temperature = 0.0;
322        let mut measurement_error_water_temperature: bool;
323        let mut ventilation_state: ActuatorState = ActuatorState::Undefined;
324        let mut schedule_check_result: bool;
325        let mut ventilation_inhibited = false; // state of ventilation control determined by if the start/stop command has been received
326        let mut lock_warn_cycle_time_exceeded = false;
327        let mut start_time = Instant::now();
328        let mut actuation_mutex_was_blocked_during_actuation: bool = false;
329
330        loop {
331            let (quit_command_received, start_command_received, stop_command_received) = self
332                .process_external_request(
333                    &mut ventilation_channels.rx_ventilation_from_signal_handler,
334                    ventilation_channels
335                        .rx_ventilation_from_messaging_opt
336                        .as_mut(),
337                );
338            if quit_command_received {
339                break;
340            }
341            if stop_command_received {
342                #[cfg(not(test))]
343                info!(
344                    target: module_path!(),
345                    "received Stop command. Inhibiting ventilation."
346                );
347                ventilation_inhibited = true;
348            }
349            if start_command_received {
350                #[cfg(not(test))]
351                info!(
352                    target: module_path!(),
353                    "received Start command. Restarting ventilation."
354                );
355                ventilation_inhibited = false;
356            }
357
358            if self.config.active {
359                // read water temperature from mutex
360                {
361                    match mutex_sensor_manager_signals.lock() {
362                        Ok(c) => {
363                            measurement_error_water_temperature = false;
364                            measured_water_temperature = c.water_temperature;
365                        }
366                        Err(_) => {
367                            measurement_error_water_temperature = true;
368                        }
369                    };
370                }
371
372                // check if set values need to be updated
373                update_thermal_set_values!(
374                    ventilation_set_val_updater,
375                    self.config.switch_off_temperature,
376                    self.config.switch_on_temperature,
377                    self.lock_error_ventilation_set_value_read_failure,
378                    module_path!()
379                );
380
381                // schedule check to see if actuation is allowed
382                perform_schedule_check!(
383                    ventilation_channels,
384                    schedule_check_result,
385                    self.lock_error_channel_send_schedule_check,
386                    self.lock_error_channel_receive_schedule_check,
387                    module_path!()
388                );
389
390                if schedule_check_result && !ventilation_inhibited {
391                    // actuation is allowed. proceed with control.
392                    if (measured_water_temperature >= self.config.switch_off_temperature)
393                        && (measured_water_temperature <= self.config.switch_on_temperature)
394                    {
395                        // temperature is within (dynamic) control window
396                        if (self.calc_normalized_control_deviation(measured_water_temperature)
397                            > saw_tooth_profile.level_normalized)
398                            && !measurement_error_water_temperature
399                        {
400                            // switch on ventilation if not already on
401                            if ventilation_state != ActuatorState::On {
402                                switch_on_ventilation(
403                                    &mutex_device_scheduler_ventilation,
404                                    &mut actuation_mutex_was_blocked_during_actuation,
405                                    ventilation_channels,
406                                );
407                                ventilation_state = ActuatorState::On;
408                            }
409                        } else {
410                            // switch off ventilation if not already off
411                            if ventilation_state != ActuatorState::Off {
412                                switch_off_ventilation(
413                                    &mutex_device_scheduler_ventilation,
414                                    &mut actuation_mutex_was_blocked_during_actuation,
415                                    ventilation_channels,
416                                );
417                                ventilation_state = ActuatorState::Off;
418                            }
419                        }
420                    } else {
421                        // The temperature is above or below (dynamic) control window
422                        if measured_water_temperature > self.config.switch_on_temperature {
423                            // switch on ventilation if not already on
424                            if ventilation_state != ActuatorState::On {
425                                switch_on_ventilation(
426                                    &mutex_device_scheduler_ventilation,
427                                    &mut actuation_mutex_was_blocked_during_actuation,
428                                    ventilation_channels,
429                                );
430                                ventilation_state = ActuatorState::On;
431                            }
432                        }
433                        if measured_water_temperature < self.config.switch_off_temperature {
434                            // switch off ventilation if not already off
435                            if ventilation_state != ActuatorState::Off {
436                                switch_off_ventilation(
437                                    &mutex_device_scheduler_ventilation,
438                                    &mut actuation_mutex_was_blocked_during_actuation,
439                                    ventilation_channels,
440                                );
441                                ventilation_state = ActuatorState::Off;
442                            }
443                        }
444                    }
445                } else if ventilation_inhibited {
446                    // An external request for stopping ventilation control has been received
447                    Self::conditional_ventilation_switch(
448                        self.config.switch_on_when_external_stop,
449                        &mut ventilation_state,
450                        &mutex_device_scheduler_ventilation,
451                        ventilation_channels,
452                        &mut actuation_mutex_was_blocked_during_actuation,
453                    );
454                } else {
455                    // schedule check requires stop of ventilation control
456                    Self::conditional_ventilation_switch(
457                        self.config.switch_on_when_out_of_schedule,
458                        &mut ventilation_state,
459                        &mutex_device_scheduler_ventilation,
460                        ventilation_channels,
461                        &mut actuation_mutex_was_blocked_during_actuation,
462                    );
463                }
464            }
465
466            let instant_before_locking_mutex = Instant::now();
467            let mut instant_after_locking_mutex = Instant::now(); // initialization is overwritten
468
469            // Write signal to mutex
470            {
471                match mutex_ventilation_status.lock() {
472                    Ok(mut c) => {
473                        instant_after_locking_mutex = Instant::now();
474                        *c = match ventilation_state {
475                            ActuatorState::On => true,
476                            ActuatorState::Off => false,
477                            _ => false,
478                        }
479                    }
480                    Err(_) => {
481                        // Do nothing
482                    }
483                }
484            }
485
486            // check if access to mutex took too long
487            self.check_mutex_access_duration(
488                None,
489                instant_after_locking_mutex,
490                instant_before_locking_mutex,
491            );
492
493            saw_tooth_profile.execute_with(TIME_INCREMENT_VENTILATION_SECS);
494
495            // Manage the loop's cycle time, sleeping or logging a warning as needed.
496            manage_cycle_time_thermal!(
497                start_time,
498                cycle_time_duration,
499                CYCLE_TIME_VENTILATION_MILLIS,
500                spin_sleeper,
501                lock_warn_cycle_time_exceeded,
502                actuation_mutex_was_blocked_during_actuation,
503                module_path!()
504            );
505        }
506
507        if self.config.switch_on_when_terminating {
508            // switch on ventilation if not already on
509            if ventilation_state != ActuatorState::On {
510                switch_on_ventilation(
511                    &mutex_device_scheduler_ventilation,
512                    &mut actuation_mutex_was_blocked_during_actuation,
513                    ventilation_channels,
514                );
515            }
516        } else {
517            // switch off ventilation if not already off
518            if ventilation_state != ActuatorState::Off {
519                switch_off_ventilation(
520                    &mutex_device_scheduler_ventilation,
521                    &mut actuation_mutex_was_blocked_during_actuation,
522                    ventilation_channels,
523                );
524            }
525        }
526
527        ventilation_channels.acknowledge_signal_handler();
528
529        // This thread has channel connections to underlying threads (relay manager, Atlas Scientific).
530        // Those threads have to stop receiving commands from this thread.
531        // The shutdown sequence is handled by the signal_handler module.
532        self.wait_for_termination(
533            &mut ventilation_channels.rx_ventilation_from_signal_handler,
534            sleep_duration_hundred_millis,
535            module_path!(),
536        );
537    }
538}
539
540#[cfg(test)]
541pub mod tests {
542    use crate::launch::channels::{AquaReceiver, AquaSender, Channels};
543    use crate::mocks::mock_relay_manager::tests::mock_relay_manager;
544    use crate::mocks::mock_schedule_check::tests::mock_schedule_check;
545    use crate::mocks::mock_ventilation::tests::MockSqlInterfaceVentilationSetVals;
546    use crate::sensors::sensor_manager::SensorManagerSignals;
547    use crate::thermal::ventilation::Ventilation;
548    use crate::utilities::channel_content::{AquariumDevice, InternalCommand};
549    use crate::utilities::common::tests::update_min_max_actuation_duration;
550    use crate::utilities::config::{read_config_file, ConfigData};
551    use crate::utilities::signal_handler::SignalHandlerChannels;
552    use all_asserts::{assert_ge, assert_le};
553    use spin_sleep::SpinSleeper;
554    use std::sync::{Arc, Mutex};
555    use std::thread;
556    use std::time::Duration;
557
558    // Prepares and returns a new `Ventilation` struct for use in test cases.
559    //
560    // This private helper function simplifies test setup by loading the generic
561    // test configuration file and then creating a `Ventilation` instance from its
562    // specific `ventilation` section.
563    //
564    // # Returns
565    // A freshly initialized `Ventilation` struct, ready for testing.
566    fn prepare_ventilation_tests() -> (ConfigData, Ventilation) {
567        // prepare struct Ventilation for subsequent tests
568        let config_for_ventilation: ConfigData =
569            read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
570        let config: ConfigData =
571            read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
572        (config, Ventilation::new(config_for_ventilation.ventilation))
573    }
574
575    // Creates a thread that simulates a test environment, managing test duration and sending shutdown signals.
576    //
577    // This private helper function is used in integration tests to control the lifecycle of the
578    // main test object (e.g., `Ventilation` module). It sleeps for a specified duration,
579    // simulating the active test run, and then sends a `Quit` command followed by a `Terminate`
580    // command to the test object, prompting its graceful shutdown.
581    // If the mutex is provided as optional parameter, then the function will intermittently block
582    // the mutex.
583    //
584    // # Arguments
585    // * `duration_seconds` - The duration (in seconds) that the test environment should "run" before initiating shutdown.
586    // * `tx_signal_handler_to_ventilation` - The sender channel used by this mock environment to send commands to the `Ventilation` test object, simulating the signal handler.
587    // * `rx_signal_handler_from_ventilation` - The receiver channel for this mock environment to receive acknowledgments back from the `Ventilation` test object.
588    // * `mutex_ventilation_status_opt` - Mutex for ventilation actuator status wrapped in Option
589    //
590    // # Returns
591    // A `thread::JoinHandle<()>` which can be used by the main test thread to wait for this mock environment thread's completion.
592    //
593    // # Panics
594    // This function will panic if:
595    // - The new thread cannot be spawned.
596    // - It fails to send the `Quit` or `Terminate` command.
597    // - It fails to receive the acknowledgment from the `Ventilation` test object.
598    fn create_test_environment(
599        duration_seconds: u64,
600        mut signal_handler_channels: SignalHandlerChannels,
601        mutex_ventilation_status_opt: Option<Arc<Mutex<bool>>>,
602    ) -> thread::JoinHandle<()> {
603        thread::Builder::new()
604            .name("test_environment".to_string())
605            .spawn(move || {
606                if mutex_ventilation_status_opt.is_some() {
607                    // caller wants the test environment to intermittently block the mutex
608                    let mutex_ventilation_status = mutex_ventilation_status_opt.unwrap();
609                    let sleep_duration_one_second = Duration::from_secs(1);
610                    let spin_sleeper = SpinSleeper::default();
611                    for i in 0..10 {
612                        if i % 2 == 0 {
613                            spin_sleeper.sleep(sleep_duration_one_second);
614                        } else {
615                            // block mutex on purpose
616                            {
617                                match mutex_ventilation_status.lock() {
618                                    Ok(_) => {
619                                        spin_sleeper.sleep(sleep_duration_one_second);
620                                    }
621                                    Err(_) => {
622                                        // Do nothing
623                                    }
624                                }
625                            }
626                        }
627                    }
628                } else {
629                    let sleep_duration_ten_seconds = Duration::from_secs(duration_seconds);
630                    let spin_sleeper = SpinSleeper::default();
631                    spin_sleeper.sleep(sleep_duration_ten_seconds);
632                }
633                let _ = signal_handler_channels.send_to_ventilation(InternalCommand::Quit);
634                signal_handler_channels.receive_from_ventilation().unwrap();
635                let _ = signal_handler_channels.send_to_ventilation(InternalCommand::Terminate);
636            })
637            .unwrap()
638    }
639
640    // Check if the ventilation is switched on when the temperature is high.
641    // Also, implicitly test if the execute function terminates after receiving Quit and Terminate commands.
642    #[test]
643    pub fn test_ventilation_with_measured_temperature_high_without_blocking_mutex() {
644        let sleep_duration_100_millis = Duration::from_millis(100);
645        let spin_sleeper = SpinSleeper::default();
646
647        let (mut config, mut ventilation) = prepare_ventilation_tests();
648
649        // replacement value is used for initializing mutex
650        config.sensor_manager.replacement_value_water_temperature = 30.0;
651
652        let mut channels = Channels::new_for_test();
653
654        // thread for mock schedule check
655        let join_handle_mock_schedule_check = thread::Builder::new()
656            .name("mock_schedule_check".to_string())
657            .spawn(move || {
658                mock_schedule_check(
659                    &mut channels.schedule_check.tx_schedule_check_to_ventilation,
660                    &mut channels.schedule_check.rx_schedule_check_from_ventilation,
661                    None,
662                    true,
663                );
664            })
665            .unwrap();
666
667        // Mutex needed for water temperature
668        let mutex_sensor_manager_signals = Arc::new(Mutex::new(SensorManagerSignals::new(
669            &config.sensor_manager,
670        )));
671
672        let mutex_device_scheduler_ventilation = Arc::new(Mutex::new(0));
673
674        // thread for mock relay manager - includes assertions
675        let join_handle_mock_relay_manager = create_mock_relay_manager_for_ventilation_on(
676            channels.relay_manager.tx_relay_manager_to_ventilation,
677            channels.relay_manager.rx_relay_manager_from_ventilation,
678        );
679
680        // thread for controlling duration of test run
681        let join_handle_test_environment = create_test_environment(
682            10,
683            channels.signal_handler,
684            None, // no blocking of mutex in this test case
685        );
686
687        spin_sleeper.sleep(sleep_duration_100_millis);
688
689        // thread for the test object
690        let join_handle_test_object = thread::Builder::new()
691            .name("test_object".to_string())
692            .spawn(move || {
693                // clone sender part of the channels to mock threads so that we can terminate them after execution of the test object
694                let mut tx_ventilation_to_schedule_check_for_test_case_finish = channels
695                    .ventilation
696                    .tx_ventilation_to_schedule_check
697                    .clone();
698                let mut tx_ventilation_to_relay_manager_for_test_case_finish =
699                    channels.ventilation.tx_ventilation_to_relay_manager.clone();
700
701                let mut ventilation_set_value_updater =
702                    MockSqlInterfaceVentilationSetVals::new(false, None, None, None);
703
704                ventilation.execute(
705                    mutex_device_scheduler_ventilation.clone(),
706                    &mut channels.ventilation,
707                    &mut ventilation_set_value_updater,
708                    mutex_sensor_manager_signals,
709                    Arc::new(Mutex::new(false)),
710                );
711
712                assert_eq!(ventilation.mutex_access_duration_exceeded, false);
713
714                // send Quit signal to mock threads because the test object has terminated
715                let _ = tx_ventilation_to_schedule_check_for_test_case_finish
716                    .send(InternalCommand::Quit);
717                let _ = tx_ventilation_to_relay_manager_for_test_case_finish
718                    .send(InternalCommand::Quit);
719                println!("* [Ventilation] checking reaction to high temperature succeeded.");
720            })
721            .unwrap();
722
723        join_handle_mock_schedule_check
724            .join()
725            .expect("Mock schedule check did not finish.");
726        join_handle_mock_relay_manager
727            .join()
728            .expect("Mock relay manager thread did not finish.");
729        join_handle_test_environment
730            .join()
731            .expect("Test environment thread did not finish.");
732        join_handle_test_object
733            .join()
734            .expect("Test object thread did not finish.");
735    }
736
737    // Creates a mock Relay Manager thread for `test_ventilation_with_measured_temperature_low`.
738    //
739    // This private helper function spawns a new thread that acts as a mock for the `RelayManager`.
740    // It's specifically configured for the `test_ventilation_with_measured_temperature_low` test case,
741    // expecting a precise sequence of actuation commands from the `Ventilation` test object:
742    // 1.  An `InternalCommand::SwitchOff(AquariumDevice::Ventilation)` command.
743    // 2.  An `InternalCommand::SwitchOn(AquariumDevice::Ventilation)` command.
744    //
745    // The mock then asserts that these commands are received in the correct order and type.
746    //
747    // # Arguments
748    // * `tx_relay_manager_to_ventilation` - The sender channel for this mock to send acknowledgments back to the `Ventilation` test object.
749    // * `rx_relay_manager_from_ventilation` - The receiver channel for this mock to get actuation commands from the `Ventilation` test object.
750    //
751    // # Returns
752    // A `thread::JoinHandle<()>` which can be used by the main test thread to wait for this mock thread's completion.
753    //
754    // # Panics
755    // This function will panic if:
756    // - The new thread cannot be spawned.
757    // - The expected commands are not received in the correct order or type.
758    // - Any other unexpected command is received.
759    fn create_mock_relay_manager(
760        mut tx_relay_manager_to_ventilation: AquaSender<bool>,
761        mut rx_relay_manager_from_ventilation: AquaReceiver<InternalCommand>,
762    ) -> thread::JoinHandle<()> {
763        thread::Builder::new()
764            .name("mock_relay_manager".to_string())
765            .spawn(move || {
766                let (mut actuation_events, mock_actuator_states) = mock_relay_manager(
767                    &mut tx_relay_manager_to_ventilation,
768                    &mut rx_relay_manager_from_ventilation,
769                );
770                assert_eq!(actuation_events.len(), 2);
771                let actuation_event = actuation_events.pop().unwrap();
772                assert_eq!(
773                    actuation_event.command,
774                    InternalCommand::SwitchOn(AquariumDevice::Ventilation)
775                );
776                let actuation_event = actuation_events.pop().unwrap();
777                assert_eq!(
778                    actuation_event.command,
779                    InternalCommand::SwitchOff(AquariumDevice::Ventilation)
780                );
781                mock_actuator_states.check_terminal_condition_ventilation();
782            })
783            .unwrap()
784    }
785
786    // Check if the ventilation is switched off with low temperature and then switched on during application termination (if configured).
787    // This test also implicitly verifies that the execute function terminates after receiving Quit and Terminate commands.
788    #[test]
789    pub fn test_ventilation_with_measured_temperature_low() {
790        let sleep_duration_100_millis = Duration::from_millis(100);
791        let spin_sleeper = SpinSleeper::default();
792
793        let (mut config, mut ventilation) = prepare_ventilation_tests();
794
795        // replacement value is used for initializing mutex
796        config.sensor_manager.replacement_value_water_temperature = 20.0;
797
798        let mut channels = Channels::new_for_test();
799
800        // thread for mock schedule check
801        let join_handle_mock_schedule_check = thread::Builder::new()
802            .name("mock_schedule_check".to_string())
803            .spawn(move || {
804                mock_schedule_check(
805                    &mut channels.schedule_check.tx_schedule_check_to_ventilation,
806                    &mut channels.schedule_check.rx_schedule_check_from_ventilation,
807                    None,
808                    true,
809                );
810            })
811            .unwrap();
812
813        // Mutex needed for water temperature
814        let mutex_sensor_manager_signals = Arc::new(Mutex::new(SensorManagerSignals::new(
815            &config.sensor_manager,
816        )));
817
818        let mutex_device_scheduler_ventilation = Arc::new(Mutex::new(0));
819
820        // thread for mock relay manager - includes assertions
821        let join_handle_mock_relay_manager = create_mock_relay_manager(
822            channels.relay_manager.tx_relay_manager_to_ventilation,
823            channels.relay_manager.rx_relay_manager_from_ventilation,
824        );
825
826        // thread for controlling duration of test run
827        let join_handle_test_environment = create_test_environment(
828            58,
829            channels.signal_handler,
830            None, // no blocking of mutex in this test case
831        );
832
833        spin_sleeper.sleep(sleep_duration_100_millis);
834
835        // thread for the test object
836        let join_handle_test_object = thread::Builder::new()
837            .name("test_object".to_string())
838            .spawn(move || {
839                // clone sender part of the channels to mock threads so that we can terminate them after execution of the test object
840                let mut tx_ventilation_to_schedule_check_for_test_case_finish = channels
841                    .ventilation
842                    .tx_ventilation_to_schedule_check
843                    .clone();
844                let mut tx_ventilation_to_relay_manager_for_test_case_finish =
845                    channels.ventilation.tx_ventilation_to_relay_manager.clone();
846
847                let mut ventilation_set_value_updater =
848                    MockSqlInterfaceVentilationSetVals::new(false, None, None, None);
849
850                ventilation.execute(
851                    mutex_device_scheduler_ventilation.clone(),
852                    &mut channels.ventilation,
853                    &mut ventilation_set_value_updater,
854                    mutex_sensor_manager_signals,
855                    Arc::new(Mutex::new(false)),
856                );
857
858                // send Quit signal to mock threads because the test object has terminated
859                let _ = tx_ventilation_to_schedule_check_for_test_case_finish
860                    .send(InternalCommand::Quit);
861                let _ = tx_ventilation_to_relay_manager_for_test_case_finish
862                    .send(InternalCommand::Quit);
863                println!("* [Ventilation] checking reaction to low temperature.");
864            })
865            .unwrap();
866
867        join_handle_mock_schedule_check
868            .join()
869            .expect("Mock schedule check did not finish.");
870        join_handle_mock_relay_manager
871            .join()
872            .expect("Mock relay manager thread did not finish.");
873        join_handle_test_environment
874            .join()
875            .expect("Test environment thread did not finish.");
876        join_handle_test_object
877            .join()
878            .expect("Test object thread did not finish.");
879    }
880
881    // Check if the ventilation is switched on and off during two cycles of the saw tooth profile.
882    // Duration of ventilation switched on shall equal the duration of ventilation switched off (1% tolerance).
883    // Also, implicitly test if the execute function terminates after receiving Quit and Terminate commands.
884    #[test]
885    pub fn test_ventilation_with_measured_temperature_50_percent() {
886        let sleep_duration_100_millis = Duration::from_millis(100);
887        let spin_sleeper = SpinSleeper::default();
888
889        let (mut config, mut ventilation) = prepare_ventilation_tests();
890
891        // replacement value is used for initializing mutex
892        config.sensor_manager.replacement_value_water_temperature = 25.0;
893
894        let mut channels = Channels::new_for_test();
895
896        // thread for mock schedule check
897        let join_handle_mock_schedule_check = thread::Builder::new()
898            .name("mock_schedule_check".to_string())
899            .spawn(move || {
900                mock_schedule_check(
901                    &mut channels.schedule_check.tx_schedule_check_to_ventilation,
902                    &mut channels.schedule_check.rx_schedule_check_from_ventilation,
903                    None,
904                    true,
905                );
906            })
907            .unwrap();
908
909        // Mutex needed for water temperature
910        let mutex_sensor_manager_signals = Arc::new(Mutex::new(SensorManagerSignals::new(
911            &config.sensor_manager,
912        )));
913
914        let mutex_device_scheduler_ventilation = Arc::new(Mutex::new(0));
915
916        // thread for mock relay manager - includes assertions
917        let join_handle_mock_relay_manager = thread::Builder::new()
918            .name("mock_relay_manager".to_string())
919            .spawn(move || {
920                let (mut actuation_events, mock_actuator_states) = mock_relay_manager(
921                    &mut channels.relay_manager.tx_relay_manager_to_ventilation,
922                    &mut channels.relay_manager.rx_relay_manager_from_ventilation,
923                );
924                println!(
925                    "Mock relay manager received {} commands:",
926                    actuation_events.len()
927                );
928                for actuation_event in actuation_events.clone() {
929                    println!("{}", actuation_event);
930                }
931                let actuation_event = actuation_events.pop().unwrap();
932                assert_eq!(
933                    actuation_event.command,
934                    InternalCommand::SwitchOn(AquariumDevice::Ventilation)
935                );
936                for actuation_event in actuation_events.clone() {
937                    println!("{}", actuation_event);
938                }
939                // (first and) last actuation events need to be ignored
940                let _last_actuation_event = actuation_events.pop().unwrap();
941                let second_last_actuation_event = actuation_events.pop().unwrap();
942                let third_last_actuation_event = actuation_events.pop().unwrap();
943                let fourth_last_actuation_event = actuation_events.pop().unwrap();
944                let fifth_last_actuation_event = actuation_events.pop().unwrap();
945                let sixth_last_actuation_event = actuation_events.pop().unwrap();
946                let seventh_last_actuation_event = actuation_events.pop().unwrap();
947
948                let mut min_actuation_duration: u128 = 100000;
949                let mut max_actuation_duration: u128 = 0;
950
951                let second_last_actuation_duration = second_last_actuation_event
952                    .time
953                    .duration_since(third_last_actuation_event.time)
954                    .as_millis();
955                (min_actuation_duration, max_actuation_duration) =
956                    update_min_max_actuation_duration(
957                        min_actuation_duration,
958                        max_actuation_duration,
959                        second_last_actuation_duration,
960                    );
961                println!(
962                    "second_last_actuation_duration={}",
963                    second_last_actuation_duration
964                );
965
966                let third_last_actuation_duration = third_last_actuation_event
967                    .time
968                    .duration_since(fourth_last_actuation_event.time)
969                    .as_millis();
970                (min_actuation_duration, max_actuation_duration) =
971                    update_min_max_actuation_duration(
972                        min_actuation_duration,
973                        max_actuation_duration,
974                        second_last_actuation_duration,
975                    );
976                println!(
977                    "third_last_actuation_duration={}",
978                    third_last_actuation_duration
979                );
980
981                let fourth_last_actuation_duration = fourth_last_actuation_event
982                    .time
983                    .duration_since(fifth_last_actuation_event.time)
984                    .as_millis();
985                (min_actuation_duration, max_actuation_duration) =
986                    update_min_max_actuation_duration(
987                        min_actuation_duration,
988                        max_actuation_duration,
989                        second_last_actuation_duration,
990                    );
991                println!(
992                    "fourth_last_actuation_duration={}",
993                    fourth_last_actuation_duration
994                );
995
996                let fifth_last_actuation_duration = fifth_last_actuation_event
997                    .time
998                    .duration_since(sixth_last_actuation_event.time)
999                    .as_millis();
1000                (min_actuation_duration, max_actuation_duration) =
1001                    update_min_max_actuation_duration(
1002                        min_actuation_duration,
1003                        max_actuation_duration,
1004                        second_last_actuation_duration,
1005                    );
1006                println!(
1007                    "fifth_last_actuation_duration={}",
1008                    fifth_last_actuation_duration
1009                );
1010
1011                let sixth_last_actuation_duration = sixth_last_actuation_event
1012                    .time
1013                    .duration_since(seventh_last_actuation_event.time)
1014                    .as_millis();
1015                (min_actuation_duration, max_actuation_duration) =
1016                    update_min_max_actuation_duration(
1017                        min_actuation_duration,
1018                        max_actuation_duration,
1019                        second_last_actuation_duration,
1020                    );
1021                println!(
1022                    "sixth_last_actuation_duration={}",
1023                    sixth_last_actuation_duration
1024                );
1025
1026                let delta_min_max_duration = max_actuation_duration - min_actuation_duration;
1027
1028                // execution timing differs between platforms.
1029                // hence, platform-specific asserts are necessary
1030                cfg_if::cfg_if! {
1031                    if #[cfg(target_os = "linux")] {
1032                        assert_le!(second_last_actuation_duration, 7600);
1033                        assert_ge!(second_last_actuation_duration, 5400);
1034                        assert_le!(third_last_actuation_duration, 7600);
1035                        assert_ge!(third_last_actuation_duration, 5400);
1036                        assert_le!(fourth_last_actuation_duration, 7600);
1037                        assert_ge!(fourth_last_actuation_duration, 5400);
1038                        assert_le!(fifth_last_actuation_duration, 7600);
1039                        assert_ge!(fifth_last_actuation_duration, 5400);
1040                        assert_le!(sixth_last_actuation_duration, 7600);
1041                        assert_ge!(sixth_last_actuation_duration, 5400);
1042                    }
1043                    else {
1044                        assert_le!(second_last_actuation_duration, 7600);
1045                        assert_ge!(second_last_actuation_duration, 5000);
1046                        assert_le!(third_last_actuation_duration, 7600);
1047                        assert_ge!(third_last_actuation_duration, 5000);
1048                        assert_le!(fourth_last_actuation_duration, 7600);
1049                        assert_ge!(fourth_last_actuation_duration, 5000);
1050                        assert_le!(fifth_last_actuation_duration, 7600);
1051                        assert_ge!(fifth_last_actuation_duration, 5000);
1052                        assert_le!(sixth_last_actuation_duration, 7600);
1053                        assert_ge!(sixth_last_actuation_duration, 5000);
1054                    }
1055                }
1056                assert_le!(delta_min_max_duration, 50);
1057                mock_actuator_states.check_terminal_condition_ventilation();
1058            })
1059            .unwrap();
1060
1061        // thread for controlling duration of test run
1062        let join_handle_test_environment = create_test_environment(
1063            58,
1064            channels.signal_handler,
1065            None, // no blocking of mutex in this test case
1066        );
1067
1068        spin_sleeper.sleep(sleep_duration_100_millis);
1069
1070        // thread for the test object
1071        let join_handle_test_object = thread::Builder::new()
1072            .name("test_object".to_string())
1073            .spawn(move || {
1074                // clone sender part of the channels to mock threads so that we can terminate them after execution of the test object
1075                let mut tx_ventilation_to_schedule_check_for_test_case_finish = channels
1076                    .ventilation
1077                    .tx_ventilation_to_schedule_check
1078                    .clone();
1079                let mut tx_ventilation_to_relay_manager_for_test_case_finish =
1080                    channels.ventilation.tx_ventilation_to_relay_manager.clone();
1081
1082                let mut ventilation_set_value_updater =
1083                    MockSqlInterfaceVentilationSetVals::new(false, None, None, None);
1084
1085                ventilation.execute(
1086                    mutex_device_scheduler_ventilation.clone(),
1087                    &mut channels.ventilation,
1088                    &mut ventilation_set_value_updater,
1089                    mutex_sensor_manager_signals,
1090                    Arc::new(Mutex::new(false)),
1091                );
1092
1093                // send Quit signal to mock threads because the test object has terminated
1094                let _ = tx_ventilation_to_schedule_check_for_test_case_finish
1095                    .send(InternalCommand::Quit);
1096                let _ = tx_ventilation_to_relay_manager_for_test_case_finish
1097                    .send(InternalCommand::Quit);
1098                println!("* [Heating] checking reaction to medium temperature succeeded.");
1099            })
1100            .unwrap();
1101
1102        join_handle_mock_schedule_check
1103            .join()
1104            .expect("Mock schedule check did not finish.");
1105        join_handle_mock_relay_manager
1106            .join()
1107            .expect("Mock relay manager thread did not finish.");
1108        join_handle_test_environment
1109            .join()
1110            .expect("Test environment thread did not finish.");
1111        join_handle_test_object
1112            .join()
1113            .expect("Test object thread did not finish.");
1114    }
1115
1116    // Check if ventilation control is only actuating when quitting while schedule check provides a negative result.
1117    #[test]
1118    pub fn test_ventilation_block_by_schedule() {
1119        let sleep_duration_100_millis = Duration::from_millis(100);
1120        let spin_sleeper = SpinSleeper::default();
1121
1122        let (mut config, mut ventilation) = prepare_ventilation_tests();
1123
1124        // replacement value is used for initializing mutex
1125        config.sensor_manager.replacement_value_water_temperature = 30.0;
1126
1127        let mut channels = Channels::new_for_test();
1128
1129        // thread for mock schedule check
1130        let join_handle_mock_schedule_check = thread::Builder::new()
1131            .name("mock_schedule_check".to_string())
1132            .spawn(move || {
1133                mock_schedule_check(
1134                    &mut channels.schedule_check.tx_schedule_check_to_ventilation,
1135                    &mut channels.schedule_check.rx_schedule_check_from_ventilation,
1136                    None,
1137                    false,
1138                );
1139            })
1140            .unwrap();
1141
1142        // Mutex needed for water temperature
1143        let mutex_sensor_manager_signals = Arc::new(Mutex::new(SensorManagerSignals::new(
1144            &config.sensor_manager,
1145        )));
1146
1147        let mutex_device_scheduler_ventilation = Arc::new(Mutex::new(0));
1148
1149        // thread for mock relay manager - includes assertions
1150        let join_handle_mock_relay_manager = create_mock_relay_manager(
1151            channels.relay_manager.tx_relay_manager_to_ventilation,
1152            channels.relay_manager.rx_relay_manager_from_ventilation,
1153        );
1154
1155        // thread for controlling duration of test run
1156        let join_handle_test_environment = create_test_environment(
1157            10,
1158            channels.signal_handler,
1159            None, // no blocking of mutex in this test case
1160        );
1161
1162        spin_sleeper.sleep(sleep_duration_100_millis);
1163
1164        // thread for the test object
1165        let join_handle_test_object = thread::Builder::new()
1166            .name("test_object".to_string())
1167            .spawn(move || {
1168                // clone sender part of the channels to mock threads so that we can terminate them after execution of the test object
1169                let mut tx_ventilation_to_schedule_check_for_test_case_finish = channels
1170                    .ventilation
1171                    .tx_ventilation_to_schedule_check
1172                    .clone();
1173                let mut tx_ventilation_to_relay_manager_for_test_case_finish =
1174                    channels.ventilation.tx_ventilation_to_relay_manager.clone();
1175
1176                let mut ventilation_set_value_updater =
1177                    MockSqlInterfaceVentilationSetVals::new(false, None, None, None);
1178
1179                ventilation.execute(
1180                    mutex_device_scheduler_ventilation.clone(),
1181                    &mut channels.ventilation,
1182                    &mut ventilation_set_value_updater,
1183                    mutex_sensor_manager_signals,
1184                    Arc::new(Mutex::new(false)),
1185                );
1186
1187                // send Quit signal to mock threads because the test object has terminated
1188                let _ = tx_ventilation_to_schedule_check_for_test_case_finish
1189                    .send(InternalCommand::Quit);
1190                let _ = tx_ventilation_to_relay_manager_for_test_case_finish
1191                    .send(InternalCommand::Quit);
1192
1193                let actuation_count = *mutex_device_scheduler_ventilation.lock().unwrap();
1194
1195                // check if ventilation is only actuated once (when quitting)
1196                assert_eq!(actuation_count, 2);
1197
1198                println!("* [Ventilation] checking if schedule checker can block actuation.");
1199            })
1200            .unwrap();
1201
1202        join_handle_mock_schedule_check
1203            .join()
1204            .expect("Mock schedule check did not finish.");
1205        join_handle_mock_relay_manager
1206            .join()
1207            .expect("Mock relay manager thread did not finish.");
1208        join_handle_test_environment
1209            .join()
1210            .expect("Test environment thread did not finish.");
1211        join_handle_test_object
1212            .join()
1213            .expect("Test object thread did not finish.");
1214    }
1215
1216    // Test case runs ventilation control and triggers inhibition by sending the stop message via the channel.
1217    // After verification that ventilation is stopped,
1218    // The test case sends the start message via the channel and verifies if ventilation control
1219    // resumes operation.
1220    #[test]
1221    pub fn test_messaging_stops_starts_ventilation() {
1222        let mut config: ConfigData =
1223            read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
1224
1225        // replacement value is used for initialization of mutex
1226        config.sensor_manager.replacement_value_water_temperature = 30.0;
1227
1228        // Mutex for water temperature normally coming from Atlas Scientific
1229        let mutex_sensor_manager_signals = Arc::new(Mutex::new(SensorManagerSignals::new(
1230            &config.sensor_manager,
1231        )));
1232
1233        let mut ventilation = Ventilation::new(config.ventilation);
1234
1235        let mut channels = Channels::new_for_test();
1236
1237        let mutex_device_scheduler_ventilation = Arc::new(Mutex::new(0));
1238        let mutex_device_scheduler_test_environment = mutex_device_scheduler_ventilation.clone();
1239
1240        // thread for mock schedule check
1241        let join_handle_mock_schedule_check = thread::Builder::new()
1242            .name("mock_schedule_check".to_string())
1243            .spawn(move || {
1244                mock_schedule_check(
1245                    &mut channels.schedule_check.tx_schedule_check_to_ventilation,
1246                    &mut channels.schedule_check.rx_schedule_check_from_ventilation,
1247                    None,
1248                    true,
1249                );
1250            })
1251            .unwrap();
1252
1253        // thread for mock relay manager - includes assertions
1254        let join_handle_mock_relay_manager = thread::Builder::new()
1255            .name("mock_relay_manager".to_string())
1256            .spawn(move || {
1257                let (mut actuation_events, mock_actuator_states) = mock_relay_manager(
1258                    &mut channels.relay_manager.tx_relay_manager_to_ventilation,
1259                    &mut channels.relay_manager.rx_relay_manager_from_ventilation,
1260                );
1261                println!("actuation_events:");
1262                for actuation_event in &actuation_events {
1263                    println!("{}", actuation_event);
1264                }
1265                assert_eq!(actuation_events.len(), 5);
1266                let actuation_event = actuation_events.pop().unwrap();
1267                assert_eq!(
1268                    actuation_event.command,
1269                    InternalCommand::SwitchOn(AquariumDevice::Ventilation)
1270                );
1271                let actuation_event = actuation_events.pop().unwrap();
1272                assert_eq!(
1273                    actuation_event.command,
1274                    InternalCommand::SwitchOff(AquariumDevice::Ventilation)
1275                );
1276                let actuation_event = actuation_events.pop().unwrap();
1277                assert_eq!(
1278                    actuation_event.command,
1279                    InternalCommand::SwitchOn(AquariumDevice::Ventilation)
1280                );
1281                let actuation_event = actuation_events.pop().unwrap();
1282                assert_eq!(
1283                    actuation_event.command,
1284                    InternalCommand::SwitchOff(AquariumDevice::Ventilation)
1285                );
1286                let actuation_event = actuation_events.pop().unwrap();
1287                assert_eq!(
1288                    actuation_event.command,
1289                    InternalCommand::SwitchOn(AquariumDevice::Ventilation)
1290                );
1291                mock_actuator_states.check_terminal_condition_ventilation();
1292            })
1293            .unwrap();
1294
1295        // thread for test environment
1296        let join_handle_test_environment = thread::Builder::new()
1297            .name("test_environment".to_string())
1298            .spawn(move || {
1299                let sleep_duration_100_millis = Duration::from_millis(100);
1300                let spin_sleeper = SpinSleeper::default();
1301
1302                // initial wait so that ventilation control can switch on
1303                for _ in 0..10 {
1304                    spin_sleeper.sleep(sleep_duration_100_millis);
1305                }
1306                // check the initial state of mutex: should be already switched on
1307                assert_eq!(*mutex_device_scheduler_test_environment.lock().unwrap(), 1);
1308
1309                // sending a message requesting stop of ventilation control
1310                match channels
1311                    .messaging
1312                    .tx_messaging_to_ventilation
1313                    .send(InternalCommand::Stop)
1314                {
1315                    Ok(()) => { /* do nothing */ }
1316                    Err(e) => {
1317                        panic!(
1318                            "{}: error when sending stop command to test object ({e:?})",
1319                            module_path!()
1320                        );
1321                    }
1322                }
1323
1324                // wait for 1 second: Ventilation control will switch off
1325                for _ in 0..10 {
1326                    spin_sleeper.sleep(sleep_duration_100_millis);
1327                }
1328                // check if ventilation is actuated
1329                assert_eq!(*mutex_device_scheduler_test_environment.lock().unwrap(), 2);
1330
1331                // sending a message requesting restart of ventilation control
1332                match channels
1333                    .messaging
1334                    .tx_messaging_to_ventilation
1335                    .send(InternalCommand::Start)
1336                {
1337                    Ok(()) => { /* do nothing */ }
1338                    Err(e) => {
1339                        panic!(
1340                            "{}: error when sending start command to test object ({e:?})",
1341                            module_path!()
1342                        );
1343                    }
1344                }
1345
1346                // Wait for 1 second. Ventilation control will switch on
1347                for _ in 0..10 {
1348                    spin_sleeper.sleep(sleep_duration_100_millis);
1349                }
1350                // check if ventilation is actuated
1351                assert_eq!(*mutex_device_scheduler_test_environment.lock().unwrap(), 3);
1352
1353                // sending a message requesting stop of ventilation control
1354                match channels
1355                    .messaging
1356                    .tx_messaging_to_ventilation
1357                    .send(InternalCommand::Stop)
1358                {
1359                    Ok(()) => { /* do nothing */ }
1360                    Err(e) => {
1361                        panic!(
1362                            "{}: error when sending stop command to test object ({e:?})",
1363                            module_path!()
1364                        );
1365                    }
1366                }
1367
1368                // wait for 1 second: Ventilation control will switch off
1369                for _ in 0..10 {
1370                    spin_sleeper.sleep(sleep_duration_100_millis);
1371                }
1372                // check if ventilation is actuated
1373                assert_eq!(*mutex_device_scheduler_test_environment.lock().unwrap(), 4);
1374
1375                // requesting ventilation control to quit
1376                let _ = channels
1377                    .signal_handler
1378                    .send_to_ventilation(InternalCommand::Quit);
1379                channels.signal_handler.receive_from_ventilation().unwrap();
1380
1381                // check if ventilation is actuated
1382                assert_eq!(*mutex_device_scheduler_test_environment.lock().unwrap(), 5);
1383                let _ = channels
1384                    .signal_handler
1385                    .send_to_ventilation(InternalCommand::Terminate);
1386            })
1387            .unwrap();
1388
1389        // thread for the test object
1390        let join_handle_test_object = thread::Builder::new()
1391            .name("test_object".to_string())
1392            .spawn(move || {
1393                // clone sender part of the channels to mock threads so that we can terminate them after execution of the test object
1394                let mut tx_ventilation_to_schedule_check_for_test_case_finish = channels
1395                    .ventilation
1396                    .tx_ventilation_to_schedule_check
1397                    .clone();
1398                let mut tx_ventilation_to_relay_manager_for_test_case_finish =
1399                    channels.ventilation.tx_ventilation_to_relay_manager.clone();
1400
1401                let mut ventilation_set_value_updater =
1402                    MockSqlInterfaceVentilationSetVals::new(false, None, None, None);
1403
1404                ventilation.execute(
1405                    mutex_device_scheduler_ventilation.clone(),
1406                    &mut channels.ventilation,
1407                    &mut ventilation_set_value_updater,
1408                    mutex_sensor_manager_signals,
1409                    Arc::new(Mutex::new(false)),
1410                );
1411
1412                // send Quit signal to mock threads because the test object has terminated
1413                let _ = tx_ventilation_to_schedule_check_for_test_case_finish
1414                    .send(InternalCommand::Quit);
1415                let _ = tx_ventilation_to_relay_manager_for_test_case_finish
1416                    .send(InternalCommand::Quit);
1417
1418                println!("* [Ventilation] checking if messaging can block and restart actuation.");
1419            })
1420            .unwrap();
1421
1422        join_handle_mock_schedule_check
1423            .join()
1424            .expect("Mock schedule check did not finish.");
1425        join_handle_mock_relay_manager
1426            .join()
1427            .expect("Mock relay manager thread did not finish.");
1428        join_handle_test_environment
1429            .join()
1430            .expect("Test environment thread did not finish.");
1431        join_handle_test_object
1432            .join()
1433            .expect("Test object thread did not finish.");
1434    }
1435
1436    // Check if the ventilation is switched on when the temperature is high.
1437    // After a limited time period, increase the set values and observe if the heater is switched off.
1438    // Also, implicitly test if the execute function terminates after receiving Quit and Terminate commands.
1439    // Test case uses test database #52.
1440    #[test]
1441    pub fn test_ventilation_with_decreased_set_values() {
1442        let sleep_duration_100_millis = Duration::from_millis(100);
1443        let spin_sleeper = SpinSleeper::default();
1444
1445        let (mut config, mut ventilation) = prepare_ventilation_tests();
1446
1447        // replacement value is used for initializing mutex
1448        config.sensor_manager.replacement_value_water_temperature = 30.0;
1449
1450        let mut channels = Channels::new_for_test();
1451
1452        // thread for mock schedule check
1453        let join_handle_mock_schedule_check = thread::Builder::new()
1454            .name("mock_schedule_check".to_string())
1455            .spawn(move || {
1456                mock_schedule_check(
1457                    &mut channels.schedule_check.tx_schedule_check_to_ventilation,
1458                    &mut channels.schedule_check.rx_schedule_check_from_ventilation,
1459                    None,
1460                    true,
1461                );
1462            })
1463            .unwrap();
1464
1465        // Mutex needed for water temperature
1466        let mutex_sensor_manager_signals = Arc::new(Mutex::new(SensorManagerSignals::new(
1467            &config.sensor_manager,
1468        )));
1469
1470        let mutex_device_scheduler_ventilation = Arc::new(Mutex::new(0));
1471
1472        // thread for mock relay manager - includes assertions
1473        let join_handle_mock_relay_manager = thread::Builder::new()
1474            .name("mock_relay_manager".to_string())
1475            .spawn(move || {
1476                let (mut actuation_events, mock_actuator_states) = mock_relay_manager(
1477                    &mut channels.relay_manager.tx_relay_manager_to_ventilation,
1478                    &mut channels.relay_manager.rx_relay_manager_from_ventilation,
1479                );
1480                assert_eq!(actuation_events.len(), 3);
1481                let actuation_event_3 = actuation_events.pop().unwrap();
1482                let actuation_event_2 = actuation_events.pop().unwrap();
1483                let actuation_event_1 = actuation_events.pop().unwrap();
1484                assert_eq!(
1485                    actuation_event_1.command,
1486                    InternalCommand::SwitchOn(AquariumDevice::Ventilation)
1487                );
1488                assert_eq!(
1489                    actuation_event_2.command,
1490                    InternalCommand::SwitchOff(AquariumDevice::Ventilation)
1491                );
1492                assert_eq!(
1493                    actuation_event_3.command,
1494                    InternalCommand::SwitchOn(AquariumDevice::Ventilation)
1495                );
1496                mock_actuator_states.check_terminal_condition_ventilation();
1497            })
1498            .unwrap();
1499
1500        // thread for controlling duration of test run
1501        let join_handle_test_environment = create_test_environment(
1502            12,
1503            channels.signal_handler,
1504            None, // no blocking of mutex in this test case
1505        );
1506
1507        spin_sleeper.sleep(sleep_duration_100_millis);
1508
1509        // thread for the test object
1510        let join_handle_test_object = thread::Builder::new()
1511            .name("test_object".to_string())
1512            .spawn(move || {
1513                // clone sender part of the channels to mock threads so that we can terminate them after execution of the test object
1514                let mut tx_ventilation_to_schedule_check_for_test_case_finish = channels
1515                    .ventilation
1516                    .tx_ventilation_to_schedule_check
1517                    .clone();
1518                let mut tx_ventilation_to_relay_manager_for_test_case_finish =
1519                    channels.ventilation.tx_ventilation_to_relay_manager.clone();
1520
1521                // set up the set value updater to actively manipulate the set values
1522                let mut ventilation_set_value_updater = MockSqlInterfaceVentilationSetVals::new(
1523                    true,
1524                    Some(30.0),
1525                    Some(40.0),
1526                    Some(Duration::from_secs(10)),
1527                );
1528
1529                ventilation.execute(
1530                    mutex_device_scheduler_ventilation.clone(),
1531                    &mut channels.ventilation,
1532                    &mut ventilation_set_value_updater,
1533                    mutex_sensor_manager_signals,
1534                    Arc::new(Mutex::new(false)),
1535                );
1536
1537                // send Quit signal to mock threads because the test object has terminated
1538                let _ = tx_ventilation_to_schedule_check_for_test_case_finish
1539                    .send(InternalCommand::Quit);
1540                let _ = tx_ventilation_to_relay_manager_for_test_case_finish
1541                    .send(InternalCommand::Quit);
1542                println!("* [Ventilation] checking reaction to high temperature succeeded.");
1543            })
1544            .unwrap();
1545
1546        join_handle_mock_schedule_check
1547            .join()
1548            .expect("Mock schedule check did not finish.");
1549        join_handle_mock_relay_manager
1550            .join()
1551            .expect("Mock relay manager thread did not finish.");
1552        join_handle_test_environment
1553            .join()
1554            .expect("Test environment thread did not finish.");
1555        join_handle_test_object
1556            .join()
1557            .expect("Test object thread did not finish.");
1558    }
1559
1560    // helper function to avoid warning about duplicated code.
1561    fn create_mock_relay_manager_for_ventilation_on(
1562        mut tx_relay_manager_to_ventilation: AquaSender<bool>,
1563        mut rx_relay_manager_from_ventilation: AquaReceiver<InternalCommand>,
1564    ) -> thread::JoinHandle<()> {
1565        thread::Builder::new()
1566            .name("mock_relay_manager".to_string())
1567            .spawn(move || {
1568                let (mut actuation_events, mock_actuator_states) = mock_relay_manager(
1569                    &mut tx_relay_manager_to_ventilation,
1570                    &mut rx_relay_manager_from_ventilation,
1571                );
1572                assert_eq!(actuation_events.len(), 1);
1573                let actuation_event = actuation_events.pop().unwrap();
1574                assert_eq!(
1575                    actuation_event.command,
1576                    InternalCommand::SwitchOn(AquariumDevice::Ventilation)
1577                );
1578                mock_actuator_states.check_terminal_condition_ventilation();
1579            })
1580            .unwrap()
1581    }
1582
1583    // Check if the ventilation is switched on when the temperature is high.
1584    // Also, implicitly test if the execute function terminates after receiving Quit and Terminate commands.
1585    #[test]
1586    pub fn test_ventilation_with_measured_temperature_high_with_blocking_mutex() {
1587        let sleep_duration_100_millis = Duration::from_millis(100);
1588        let spin_sleeper = SpinSleeper::default();
1589
1590        let (mut config, mut ventilation) = prepare_ventilation_tests();
1591
1592        // replacement value is used for initializing mutex
1593        config.sensor_manager.replacement_value_water_temperature = 30.0;
1594
1595        let mut channels = Channels::new_for_test();
1596
1597        let mutex_ventilation_status = Arc::new(Mutex::new(false));
1598        let mutex_ventilation_status_clone_for_test_environment = mutex_ventilation_status.clone();
1599
1600        // thread for mock schedule check
1601        let join_handle_mock_schedule_check = thread::Builder::new()
1602            .name("mock_schedule_check".to_string())
1603            .spawn(move || {
1604                mock_schedule_check(
1605                    &mut channels.schedule_check.tx_schedule_check_to_ventilation,
1606                    &mut channels.schedule_check.rx_schedule_check_from_ventilation,
1607                    None,
1608                    true,
1609                );
1610            })
1611            .unwrap();
1612
1613        // Mutex needed for water temperature
1614        let mutex_sensor_manager_signals = Arc::new(Mutex::new(SensorManagerSignals::new(
1615            &config.sensor_manager,
1616        )));
1617
1618        let mutex_device_scheduler_ventilation = Arc::new(Mutex::new(0));
1619
1620        // thread for mock relay manager - includes assertions
1621        let join_handle_mock_relay_manager = create_mock_relay_manager_for_ventilation_on(
1622            channels.relay_manager.tx_relay_manager_to_ventilation,
1623            channels.relay_manager.rx_relay_manager_from_ventilation,
1624        );
1625
1626        // thread for controlling duration of test run
1627        let join_handle_test_environment = create_test_environment(
1628            10,
1629            channels.signal_handler,
1630            Some(mutex_ventilation_status_clone_for_test_environment), // blocking of mutex in this test case
1631        );
1632
1633        spin_sleeper.sleep(sleep_duration_100_millis);
1634
1635        // thread for the test object
1636        let join_handle_test_object = thread::Builder::new()
1637            .name("test_object".to_string())
1638            .spawn(move || {
1639                // clone sender part of the channels to mock threads so that we can terminate them after execution of the test object
1640                let mut tx_ventilation_to_schedule_check_for_test_case_finish = channels
1641                    .ventilation
1642                    .tx_ventilation_to_schedule_check
1643                    .clone();
1644                let mut tx_ventilation_to_relay_manager_for_test_case_finish =
1645                    channels.ventilation.tx_ventilation_to_relay_manager.clone();
1646
1647                let mut ventilation_set_value_updater =
1648                    MockSqlInterfaceVentilationSetVals::new(false, None, None, None);
1649
1650                ventilation.execute(
1651                    mutex_device_scheduler_ventilation.clone(),
1652                    &mut channels.ventilation,
1653                    &mut ventilation_set_value_updater,
1654                    mutex_sensor_manager_signals,
1655                    mutex_ventilation_status,
1656                );
1657
1658                assert_eq!(ventilation.mutex_access_duration_exceeded, true);
1659
1660                // send Quit signal to mock threads because the test object has terminated
1661                let _ = tx_ventilation_to_schedule_check_for_test_case_finish
1662                    .send(InternalCommand::Quit);
1663                let _ = tx_ventilation_to_relay_manager_for_test_case_finish
1664                    .send(InternalCommand::Quit);
1665                println!("* [Ventilation] checking reaction to high temperature succeeded.");
1666            })
1667            .unwrap();
1668
1669        join_handle_mock_schedule_check
1670            .join()
1671            .expect("Mock schedule check did not finish.");
1672        join_handle_mock_relay_manager
1673            .join()
1674            .expect("Mock relay manager thread did not finish.");
1675        join_handle_test_environment
1676            .join()
1677            .expect("Test environment thread did not finish.");
1678        join_handle_test_object
1679            .join()
1680            .expect("Test object thread did not finish.");
1681    }
1682}