aquarium_control/sensors/
sensor_manager.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#![allow(non_snake_case)]
10
11use log::error;
12#[cfg(all(target_os = "linux", not(test)))]
13use log::info;
14
15use std::sync::{Arc, Mutex};
16
17#[cfg(feature = "debug_sensor_manager")]
18use log::debug;
19
20#[cfg(all(target_os = "linux", not(test)))]
21use nix::unistd::gettid;
22
23use spin_sleep::SpinSleeper;
24
25use crate::sensors::atlas_scientific::AtlasScientificResultData;
26use crate::sensors::dht::DhtResult;
27use crate::sensors::ds18b20::Ds18b20Result;
28use crate::sensors::sensor_manager_channels::SensorManagerChannels;
29use crate::sensors::sensor_manager_config::SensorManagerConfig;
30use crate::simulator::get_resp_sim::GetResponseFromSimulatorTrait;
31use crate::simulator::tcp_communication_error::TcpCommunicationError;
32use crate::utilities::acknowledge_signal_handler::AcknowledgeSignalHandlerTrait;
33use crate::utilities::channel_content::{AquariumSignal, InternalCommand};
34use crate::utilities::check_mutex_access_duration::CheckMutexAccessDurationTrait;
35use crate::utilities::logger::log_error_chain;
36use crate::utilities::proc_ext_req::ProcessExternalRequestTrait;
37use crate::utilities::wait_for_termination::WaitForTerminationTrait;
38use std::time::{Duration, Instant};
39use thiserror::Error;
40
41mod sensor_manager_constants {
42    pub const SOURCE_WATER_TEMPERATURE_ATLAS_SCIENTIFIC: &str = "atlas_scientific";
43
44    pub const SOURCE_WATER_TEMPERATURE_DS18B20: &str = "ds18b20";
45
46    pub const SOURCE_AMBIENT_TEMPERATURE_DHT: &str = "dht";
47
48    pub const SOURCE_AMBIENT_TEMPERATURE_DS18B20: &str = "ds18b20";
49
50    pub const SIMULATOR_MEASUREMENT_INTERVAL_MILLIS: u64 = 500;
51
52    // time interval for updating measurement data
53    pub const HW_MEASUREMENT_INTERVAL_MILLIS: u64 = 500;
54
55    /// allow max. 10 milliseconds for mutex to be blocked by any other thread
56    pub const MAX_MUTEX_ACCESS_DURATION_MILLIS: u64 = 10;
57}
58
59/// Contains `Arc<Mutex>`-wrapped shared data points accessed by the sensor manager.
60///
61/// These mutexes provide thread-safe access to various sensor readings and
62/// allow the sensor manager to communicate the output to other threads.
63#[derive(Clone)]
64pub struct SensorManagerMutexes {
65    /// Mutex for ambient temperature from Ds18b20 sensor
66    pub mutex_ds18b20_ambient_temperature: Arc<Mutex<Ds18b20Result>>,
67
68    /// Mutex for water temperature from Ds18b20 sensor
69    pub mutex_ds18b20_water_temperature: Arc<Mutex<Ds18b20Result>>,
70
71    /// Mutex for ambient temperature and humidity fron DHT sensor
72    pub mutex_dht: Arc<Mutex<DhtResult>>,
73
74    /// Mutex for temperature from Atlas Scientific sensor unit
75    pub mutex_atlas_scientific_temperature: Arc<Mutex<AtlasScientificResultData>>,
76
77    /// Mutex for pH value from Atlas Scientific sensor unit
78    pub mutex_atlas_scientific_ph: Arc<Mutex<AtlasScientificResultData>>,
79
80    /// Mutex for conductivity from Atlas Scientific sensor unit
81    pub mutex_atlas_scientific_conductivity: Arc<Mutex<AtlasScientificResultData>>,
82
83    /// Mutex for output of sensor manager
84    pub mutex_sensor_manager_signals: Arc<Mutex<SensorManagerSignals>>,
85}
86
87/// Container for the signals managed by SensorManager.
88/// Controls for heating and ventilation use these signals.
89pub struct SensorManagerSignals {
90    /// Water temperature in °C either coming from Atlas Scientific or Ds18b20
91    pub water_temperature: f32,
92
93    /// Ambient temperature in °C either coming from Dht or Ds18b20
94    pub ambient_temperature: f32,
95
96    /// Ambient humidity in % coming from Dht (if enabled)
97    pub ambient_humidity: f32,
98
99    /// pH
100    pub ph: f32,
101
102    /// conductivity in mS
103    pub conductivity: f32,
104
105    /// Recording of the instant where the mutex is being written
106    pub instant_last_recording: Instant,
107}
108
109impl SensorManagerSignals {
110    pub fn new(config: &SensorManagerConfig) -> SensorManagerSignals {
111        SensorManagerSignals {
112            water_temperature: config.replacement_value_water_temperature,
113            ambient_temperature: config.replacement_value_ambient_temperature,
114            ambient_humidity: config.replacement_value_ambient_humidity,
115            ph: config.replacement_value_ph,
116            conductivity: config.replacement_value_conductivity,
117            instant_last_recording: Instant::now(),
118        }
119    }
120}
121
122#[derive(Error, Debug)]
123pub enum SensorManagerError {
124    /// Invalid configuration of ambient temperature measurement.
125    #[error("[{0}]: invalid configuration of ambient temperature. source_ambient_temperature may only have values ({1}, {2}) configured value is {3}")]
126    InvalidAmbientTemperatureConfiguration(String, String, String, String),
127
128    /// Invalid configuration of water temperature measurement.
129    #[error("[{0}]: invalid configuration of water temperature. source_ambient_temperature may only have values ({1}, {2}) configured value is {3}")]
130    InvalidWaterTemperatureConfiguration(String, String, String, String),
131
132    /// Communication with the simulator failed.
133    #[error("[{location}] Communication with simulator for {signal} failed")]
134    SimulatorCommunicationError {
135        location: String,
136        signal: AquariumSignal,
137
138        #[source]
139        source: TcpCommunicationError,
140    },
141}
142
143/// Contains the communication with DHT ambient temperature and humidity sensor.
144/// Alternatively, TCP communication is used when configured to run with the simulator.
145/// Sensor data is read periodically and stored.
146/// Requests from data logger and heating control are answered with last measured data.
147#[cfg_attr(doc, aquamarine::aquamarine)]
148/// Thread communication of this component is as follows:
149/// ```mermaid
150/// graph LR
151///     signal_handler[SignalHandler] --> sensor_manager[SensorManager]
152///     sensor_manager[SensorManager] --> signal_handler[SignalHandler]
153///     signal_handler[SignalHandler] --> tcp_communication[TcpCommunication]
154///     tcp_communication[TcpCommunication] --> signal_handler[SignalHandler]
155/// ```
156pub struct SensorManager {
157    // configuration data for Ambient
158    config: SensorManagerConfig,
159
160    /// ambient temperature in °C
161    ambient_temperature: f32,
162
163    /// humidity in %
164    ambient_humidity: f32,
165
166    /// water temperature in °C
167    water_temperature: f32,
168
169    /// conductivity in mS
170    conductivity: f32,
171
172    /// pH value
173    pH: f32,
174
175    /// inhibition flag to avoid flooding the log file with repeated messages about failure to receive termination signal via the channel
176    pub lock_error_channel_receive_termination: bool,
177
178    /// inhibition flag to avoid flooding the log file with repeated messages about having received inapplicable command via the channel from the signal handler thread
179    pub lock_warn_inapplicable_command_signal_handler: bool,
180
181    /// Recording when the last measurement took place
182    pub instant_last_measurement: Instant,
183
184    /// Duration in between two requests to either the simulator or the hardware
185    pub measurement_interval: Duration,
186
187    /// inhibition flag to avoid flooding the log file with repeated messages about failure to acquire lock on mutex for Dht readings
188    lock_error_dht_mutex: bool,
189
190    /// inhibition flag to avoid flooding the log file with repeated messages about failure to acquire lock on mutex for temperature from Atlas Scientific sensor unit
191    lock_error_atlas_scientific_temperature_mutex: bool,
192
193    /// inhibition flag to avoid flooding the log file with repeated messages about failure to acquire lock on mutex pH value from Atlas Scientific sensor unit
194    lock_error_atlas_scientific_ph_mutex: bool,
195
196    /// inhibition flag to avoid flooding the log file with repeated messages about failure to acquire lock on mutex for conductivity from Atlas Scientific sensor unit
197    lock_error_atlas_scientific_conductivity_mutex: bool,
198
199    /// inhibition flag to avoid flooding the log file with repeated messages about failure to acquire lock on mutex for water temperature from Ds18b20 sensor
200    lock_error_ds18b20_water_temperature_mutex: bool,
201
202    /// inhibition flag to avoid flooding the log file with repeated messages about failure to acquire lock on mutex for ambient temperature from Ds18b20 sensor
203    lock_error_ds18b20_ambient_temperature_mutex: bool,
204
205    /// inhibition flag to avoid flooding the log file with repeated messages about excessive access time to mutex
206    pub lock_warn_max_mutex_access_duration: bool,
207
208    // for testing purposes: record when mutex access time is exceeded without resetting it
209    #[cfg(test)]
210    pub mutex_access_duration_exceeded: bool,
211
212    /// Maximum permissible duration for accessing the mutex
213    pub max_mutex_access_duration: Duration,
214}
215
216impl ProcessExternalRequestTrait for SensorManager {}
217
218impl GetResponseFromSimulatorTrait for SensorManager {}
219
220impl SensorManager {
221    /// Creates a new `SensorManager` control instance.
222    ///
223    /// This constructor initializes the ambient temperature and humidity measurement module.
224    /// It sets the initial temperature and humidity values to configured replacement values
225    /// and initializes all internal "lock" flags to `false`. These flags help prevent
226    /// log flooding from recurring errors or warnings during operation.
227    ///
228    /// # Returns
229    /// A `Result` containing a new `SensorManager` instance on success, or a
230    /// `SensorManagerError` if the configuration is invalid.
231    ///
232    /// # Errors
233    /// This function will return an error if:
234    /// - `source_ambient_temperature` in the config is not a valid, known source.
235    /// - `source_water_temperature` in the config is not a valid, known source.
236    pub fn new(config: SensorManagerConfig) -> Result<SensorManager, SensorManagerError> {
237        // Check if the configuration of the ambient temperature source is valid.
238        if config.source_ambient_temperature
239            != sensor_manager_constants::SOURCE_AMBIENT_TEMPERATURE_DHT
240            && config.source_ambient_temperature
241                != sensor_manager_constants::SOURCE_AMBIENT_TEMPERATURE_DS18B20
242        {
243            return Err(SensorManagerError::InvalidAmbientTemperatureConfiguration(
244                module_path!().to_string(),
245                sensor_manager_constants::SOURCE_AMBIENT_TEMPERATURE_DHT.to_string(),
246                sensor_manager_constants::SOURCE_AMBIENT_TEMPERATURE_DS18B20.to_string(),
247                config.source_ambient_temperature,
248            ));
249        }
250
251        // Check if the configuration of the water temperature source is valid.
252        if config.source_water_temperature
253            != sensor_manager_constants::SOURCE_WATER_TEMPERATURE_ATLAS_SCIENTIFIC
254            && config.source_water_temperature
255                != sensor_manager_constants::SOURCE_WATER_TEMPERATURE_DS18B20
256        {
257            return Err(SensorManagerError::InvalidWaterTemperatureConfiguration(
258                module_path!().to_string(),
259                sensor_manager_constants::SOURCE_WATER_TEMPERATURE_ATLAS_SCIENTIFIC.to_string(),
260                sensor_manager_constants::SOURCE_WATER_TEMPERATURE_DS18B20.to_string(),
261                config.source_water_temperature,
262            ));
263        }
264
265        let replacement_value_ambient_temperature = config.replacement_value_ambient_temperature;
266        let replacement_value_ambient_humidity = config.replacement_value_ambient_humidity;
267
268        // define the measurement interval depending on if simulator or hardware shall be used
269        let measurement_interval = if config.use_simulator {
270            Duration::from_millis(sensor_manager_constants::SIMULATOR_MEASUREMENT_INTERVAL_MILLIS)
271        } else {
272            Duration::from_millis(sensor_manager_constants::HW_MEASUREMENT_INTERVAL_MILLIS)
273        };
274
275        Ok(SensorManager {
276            config,
277            ambient_temperature: replacement_value_ambient_temperature,
278            ambient_humidity: replacement_value_ambient_humidity,
279            water_temperature: 0.0,
280            lock_error_channel_receive_termination: false,
281            lock_warn_inapplicable_command_signal_handler: false,
282            lock_error_dht_mutex: false,
283            conductivity: 0.0,
284            pH: 0.0,
285            instant_last_measurement: Instant::now(),
286            measurement_interval,
287            lock_error_atlas_scientific_temperature_mutex: false,
288            lock_error_atlas_scientific_ph_mutex: false,
289            lock_error_atlas_scientific_conductivity_mutex: false,
290            lock_error_ds18b20_water_temperature_mutex: false,
291            lock_error_ds18b20_ambient_temperature_mutex: false,
292            lock_warn_max_mutex_access_duration: false,
293            #[cfg(test)]
294            mutex_access_duration_exceeded: false,
295            max_mutex_access_duration: Duration::from_millis(
296                sensor_manager_constants::MAX_MUTEX_ACCESS_DURATION_MILLIS,
297            ),
298        })
299    }
300
301    /// Communicates with a simulator thread via channels to retrieve
302    /// water temperature, ambient temperature, ambient humidity, pH and conductivity.
303    ///
304    /// This private helper function is used when the `SensorManager` module is configured
305    /// to run in simulation mode. It sends `RequestSignal` commands to the simulator and
306    /// updates the module's internal fields with the responses.
307    ///
308    /// # Arguments
309    /// * `tx_sensor_manager_to_tcp_opt` - A reference to an `Option<Sender<InternalCommand>>` used
310    ///   for sending commands to the `TcpCommunication` thread (which acts as the simulator interface).
311    /// * `rx_sensor_manager_from_tcp_opt` - A reference to an `Option<Receiver<Result<f32, TcpCommunicationError>>>`
312    ///   used for receiving simulated sensor data from the `TcpCommunication` thread.
313    ///
314    /// # Returns
315    /// An empty `Result` on success, or a `SensorManagerError` if communication fails.
316    ///
317    /// # Errors
318    /// This function will return a `SensorManagerError::SimulatorCommunicationError` if sending a
319    /// request or receiving a response over the channels fails.
320    ///
321    /// # Panics
322    /// This function will **panic** if either `tx_sensor_manager_to_tcp_opt` or `rx_sensor_manager_from_tcp_opt`
323    /// are `None`. This case is not possible because the `SensorManager` module performs checks beforehand to ensure
324    /// these channels are `Some` when running in simulator mode.
325    fn communicate_with_simulator(
326        &mut self,
327        sensor_manager_channels: &mut SensorManagerChannels,
328    ) -> Result<(), SensorManagerError> {
329        // unwrap is safe because tested beforehand
330        let tx_sensor_manager_to_simulator = sensor_manager_channels
331            .tx_sensor_manager_to_tcp_opt
332            .as_mut()
333            .unwrap();
334        let rx_sensor_manager_from_simulator = sensor_manager_channels
335            .rx_sensor_manager_from_tcp_opt
336            .as_mut()
337            .unwrap();
338
339        self.ambient_temperature = Self::get_response_from_simulator(
340            module_path!().to_string(),
341            tx_sensor_manager_to_simulator,
342            rx_sensor_manager_from_simulator,
343            InternalCommand::RequestSignal(AquariumSignal::AmbientTemperature),
344        )
345        .map_err(|e| SensorManagerError::SimulatorCommunicationError {
346            location: module_path!().to_string(),
347            signal: AquariumSignal::AmbientTemperature,
348            source: e,
349        })?;
350        self.ambient_humidity = Self::get_response_from_simulator(
351            module_path!().to_string(),
352            tx_sensor_manager_to_simulator,
353            rx_sensor_manager_from_simulator,
354            InternalCommand::RequestSignal(AquariumSignal::AmbientHumidity),
355        )
356        .map_err(|e| SensorManagerError::SimulatorCommunicationError {
357            location: module_path!().to_string(),
358            signal: AquariumSignal::AmbientHumidity,
359            source: e,
360        })?;
361        self.water_temperature = Self::get_response_from_simulator(
362            module_path!().to_string(),
363            tx_sensor_manager_to_simulator,
364            rx_sensor_manager_from_simulator,
365            InternalCommand::RequestSignal(AquariumSignal::WaterTemperature),
366        )
367        .map_err(|e| SensorManagerError::SimulatorCommunicationError {
368            location: module_path!().to_string(),
369            signal: AquariumSignal::WaterTemperature,
370            source: e,
371        })?;
372        self.conductivity = Self::get_response_from_simulator(
373            module_path!().to_string(),
374            tx_sensor_manager_to_simulator,
375            rx_sensor_manager_from_simulator,
376            InternalCommand::RequestSignal(AquariumSignal::Conductivity),
377        )
378        .map_err(|e| SensorManagerError::SimulatorCommunicationError {
379            location: module_path!().to_string(),
380            signal: AquariumSignal::Conductivity,
381            source: e,
382        })?;
383        self.pH = Self::get_response_from_simulator(
384            module_path!().to_string(),
385            tx_sensor_manager_to_simulator,
386            rx_sensor_manager_from_simulator,
387            InternalCommand::RequestSignal(AquariumSignal::pH),
388        )
389        .map_err(|e| SensorManagerError::SimulatorCommunicationError {
390            location: module_path!().to_string(),
391            signal: AquariumSignal::pH,
392            source: e,
393        })?;
394
395        Ok(())
396    }
397
398    /// Executes the main control loop for the sensor manager.
399    ///
400    /// This function runs continuously, managing the periodic consolidation of sensor data.
401    /// It operates in two main modes:
402    /// - **Simulator Mode**: Fetches simulated sensor data for all signals via a TCP communication channel.
403    /// - **Hardware Mode**: Reads the latest sensor data from various shared mutexes that are
404    ///   populated by other dedicated sensor threads (like `Ds18b20`, `Dht`, `AtlasScientific`).
405    ///
406    /// Based on the configuration, it selects the appropriate data source (e.g., Ds18b20 vs.
407    /// Atlas Scientific for water temperature). The consolidated data is then written to the
408    /// `mutex_sensor_manager_signals` mutex, making it available to other application threads.
409    /// The loop maintains a measurement interval and remains responsive to `Quit` and `Terminate`
410    /// commands from the signal handler for graceful shutdown.
411    ///
412    /// # Arguments
413    /// * `sensor_manager_channels` - A struct containing all `mpsc` channels necessary for inter-thread
414    ///   communication with the `TcpCommunication` (if in simulator mode) and the `SignalHandler`.
415    /// * `mutexes` - A struct containing `Arc<Mutex<...>>` handles to all required sensor data
416    ///   sources and the final output signals struct.
417    ///
418    /// # Panics
419    /// This function will panic if:
420    /// - It's configured to use the simulator but essential TCP communication channels are missing.
421    pub fn execute(
422        &mut self,
423        sensor_manager_channels: &mut SensorManagerChannels,
424        mutexes: SensorManagerMutexes,
425    ) {
426        #[cfg(all(target_os = "linux", not(test)))]
427        info!(target: module_path!(), "Thread started with TID: {}", gettid());
428
429        if (self.config.use_simulator)
430            && (sensor_manager_channels
431                .tx_sensor_manager_to_tcp_opt
432                .is_none()
433                | sensor_manager_channels
434                    .rx_sensor_manager_from_tcp_opt
435                    .is_none())
436        {
437            panic!(
438                "{}: configured to run with simulator, but no channel to TCP thread provided.",
439                module_path!()
440            );
441        }
442
443        let sleep_duration_hundred_millis = Duration::from_millis(100);
444        let sleep_duration_one_millis = Duration::from_millis(1);
445        let spin_sleeper = SpinSleeper::default();
446        let mut mutex_poisoned = false;
447
448        // initialize signals with replacement values
449        self.ambient_temperature = self.config.replacement_value_ambient_temperature;
450        self.ambient_humidity = self.config.replacement_value_ambient_humidity;
451
452        loop {
453            let (
454                quit_command_received, // the request to end the application has been received
455                _,
456                _,
457            ) = self.process_external_request(
458                &mut sensor_manager_channels.rx_sensor_manager_from_signal_handler,
459                None,
460            );
461
462            if quit_command_received {
463                // quit command has been received
464                break;
465            }
466
467            let current_instant = Instant::now();
468            if current_instant.duration_since(self.instant_last_measurement)
469                > self.measurement_interval
470            {
471                self.instant_last_measurement = current_instant;
472                if self.config.use_simulator {
473                    let result = self.communicate_with_simulator(sensor_manager_channels);
474                    if result.is_err() {
475                        log_error_chain(
476                            module_path!(),
477                            "error when communicating with simulator",
478                            result.err().unwrap(),
479                        );
480                    }
481                } else {
482                    //*** Ds18b20 ******************************************************************
483                    if self.config.source_water_temperature
484                        == sensor_manager_constants::SOURCE_WATER_TEMPERATURE_DS18B20
485                    {
486                        match mutexes.mutex_ds18b20_water_temperature.lock() {
487                            Ok(ds18b20_water_temperature_result) => {
488                                match &*ds18b20_water_temperature_result {
489                                    Ok(ds18b20_water_temperature_result_data) => {
490                                        self.water_temperature =
491                                            ds18b20_water_temperature_result_data.value;
492                                    }
493                                    Err(_) => {
494                                        // keep the values from previous measurement
495                                    }
496                                }
497                            }
498                            Err(e) => {
499                                if !self.lock_error_ds18b20_water_temperature_mutex {
500                                    self.lock_error_ds18b20_water_temperature_mutex = true;
501                                    error!(target: module_path!(), "error when trying to lock Ds18b20 water temperature mutex: {e}");
502                                }
503                            }
504                        }
505                    }
506                    if self.config.source_ambient_temperature
507                        == sensor_manager_constants::SOURCE_WATER_TEMPERATURE_DS18B20
508                    {
509                        match mutexes.mutex_ds18b20_ambient_temperature.lock() {
510                            Ok(ds18b20_ambient_temperature_result) => {
511                                match &*ds18b20_ambient_temperature_result {
512                                    Ok(ds18b20_ambient_temperature_result_data) => {
513                                        self.ambient_temperature =
514                                            ds18b20_ambient_temperature_result_data.value;
515                                    }
516                                    Err(_) => {
517                                        // keep the values from previous measurement
518                                    }
519                                }
520                            }
521                            Err(e) => {
522                                if !self.lock_error_ds18b20_ambient_temperature_mutex {
523                                    self.lock_error_ds18b20_ambient_temperature_mutex = true;
524                                    error!(target: module_path!(), "error when trying to lock Ds18b20 ambient temperature mutex: {e}");
525                                }
526                            }
527                        }
528                    }
529                    //*** Dht **********************************************************************
530                    {
531                        match mutexes.mutex_dht.lock() {
532                            Ok(dht_result) => {
533                                match *dht_result {
534                                    Ok(tuple) => {
535                                        if self.config.source_ambient_temperature ==
536                                            sensor_manager_constants::SOURCE_AMBIENT_TEMPERATURE_DHT {
537                                            self.ambient_temperature = tuple.0;
538                                        }
539                                        self.ambient_humidity = tuple.1;
540                                    }
541                                    Err(_) => {
542                                        // keep the values from previous measurement
543                                    }
544                                }
545                            }
546                            Err(e) => {
547                                if !self.lock_error_dht_mutex {
548                                    self.lock_error_dht_mutex = true;
549                                    error!(target: module_path!(), "error when trying to lock Dht mutex: {e}");
550                                }
551                            }
552                        }
553                    }
554
555                    //*** Atlas Scientific **********************************************************
556                    {
557                        match mutexes.mutex_atlas_scientific_temperature.lock() {
558                            Ok(atlas_scientific_temperature) => {
559                                if self.config.source_water_temperature == sensor_manager_constants::SOURCE_WATER_TEMPERATURE_ATLAS_SCIENTIFIC {
560                                    self.water_temperature = atlas_scientific_temperature.value;
561                                }
562                            }
563                            Err(e) => {
564                                if !self.lock_error_atlas_scientific_temperature_mutex {
565                                    self.lock_error_atlas_scientific_temperature_mutex = true;
566                                    error!(target: module_path!(), "error when trying to lock mutex: {e}");
567                                }
568                            }
569                        }
570                    }
571
572                    {
573                        match mutexes.mutex_atlas_scientific_ph.lock() {
574                            Ok(atlas_scientific_ph) => {
575                                self.pH = atlas_scientific_ph.value;
576                            }
577                            Err(e) => {
578                                if !self.lock_error_atlas_scientific_ph_mutex {
579                                    self.lock_error_atlas_scientific_ph_mutex = true;
580                                    error!(target: module_path!(), "error when trying to lock mutex: {e}");
581                                }
582                            }
583                        }
584                    }
585
586                    {
587                        match mutexes.mutex_atlas_scientific_conductivity.lock() {
588                            Ok(atlas_scientific_conductivity) => {
589                                self.conductivity = atlas_scientific_conductivity.value;
590                            }
591                            Err(e) => {
592                                if !self.lock_error_atlas_scientific_conductivity_mutex {
593                                    self.lock_error_atlas_scientific_conductivity_mutex = true;
594                                    error!(target: module_path!(), "error when trying to lock mutex: {e}");
595                                }
596                            }
597                        }
598                    }
599                }
600
601                if !mutex_poisoned {
602                    let instant_before_locking_mutex = Instant::now();
603                    let mut instant_after_locking_mutex = Instant::now(); // initialization is overwritten
604
605                    // write the measurement result to Mutex
606                    {
607                        match mutexes.mutex_sensor_manager_signals.lock() {
608                            Ok(mut sensor_manager_signals) => {
609                                instant_after_locking_mutex = Instant::now();
610                                sensor_manager_signals.ambient_humidity = self.ambient_humidity;
611                                sensor_manager_signals.ambient_temperature =
612                                    self.ambient_temperature;
613                                sensor_manager_signals.water_temperature = self.water_temperature;
614                                sensor_manager_signals.conductivity = self.conductivity;
615                                sensor_manager_signals.ph = self.pH;
616                                sensor_manager_signals.instant_last_recording = Instant::now();
617                            }
618                            Err(e) => {
619                                log_error_chain(module_path!(), "error when trying to lock mutex for writing sensor manager data", e);
620                                mutex_poisoned = true;
621                            }
622                        }
623                    }
624
625                    // check if access to mutex took too long
626                    self.check_mutex_access_duration(
627                        None,
628                        instant_after_locking_mutex,
629                        instant_before_locking_mutex,
630                    );
631                }
632            }
633            spin_sleeper.sleep(sleep_duration_hundred_millis);
634        }
635
636        sensor_manager_channels.acknowledge_signal_handler();
637
638        // This thread has channel connections to underlying threads.
639        // Those threads have to stop receiving commands from this thread.
640        // The shutdown sequence is handled by the signal_handler module.
641        self.wait_for_termination(
642            &mut sensor_manager_channels.rx_sensor_manager_from_signal_handler,
643            sleep_duration_one_millis,
644            module_path!(),
645        );
646    }
647}
648
649#[cfg(test)]
650pub mod tests {
651    use crate::launch::channels::Channels;
652    use crate::sensors::atlas_scientific::AtlasScientificResultData;
653    use crate::sensors::ds18b20::Ds18b20ResultData;
654    use crate::sensors::sensor_manager::{
655        SensorManager, SensorManagerChannels, SensorManagerError, SensorManagerMutexes,
656        SensorManagerSignals,
657    };
658    use crate::utilities::channel_content::InternalCommand;
659    use crate::utilities::config::{read_config_file, ConfigData};
660    use crate::utilities::signal_handler_channels::SignalHandlerChannels;
661    use spin_sleep::SpinSleeper;
662    use std::sync::{Arc, Mutex};
663    use std::thread::scope;
664    use std::time::Duration;
665
666    #[test]
667    pub fn test_sensor_manager_error_water_temperature_source_is_invalid() {
668        let mut config: ConfigData =
669            read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
670        config.sensor_manager.source_water_temperature = "invalid".to_string();
671        let test_result = SensorManager::new(config.sensor_manager);
672        assert!(matches!(
673            test_result,
674            Err(SensorManagerError::InvalidWaterTemperatureConfiguration(
675                _,
676                _,
677                _,
678                _
679            ))
680        ));
681    }
682
683    #[test]
684    pub fn test_sensor_manager_error_ambient_temperature_source_is_invalid() {
685        let mut config: ConfigData =
686            read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
687        config.sensor_manager.source_ambient_temperature = "invalid".to_string();
688        let test_result = SensorManager::new(config.sensor_manager);
689        assert!(matches!(
690            test_result,
691            Err(SensorManagerError::InvalidAmbientTemperatureConfiguration(
692                _,
693                _,
694                _,
695                _
696            ))
697        ));
698    }
699
700    fn mock_signal_handler_for_sensor_manager(
701        mut signal_handler_channels: SignalHandlerChannels,
702        mutex_sensor_manager_opt: Option<Arc<Mutex<SensorManagerSignals>>>,
703    ) {
704        let spin_sleeper = SpinSleeper::default();
705        // mock signal handler
706        if mutex_sensor_manager_opt.is_none() {
707            let sleep_duration_8_seconds = Duration::from_millis(8000);
708            spin_sleeper.sleep(sleep_duration_8_seconds);
709        } else {
710            let mutex_sensor_manager = mutex_sensor_manager_opt.unwrap();
711            let sleep_duration_1_second = Duration::from_millis(1000);
712            for i in 0..8 {
713                if i % 2 > 0 {
714                    {
715                        // block the mutex on purpose
716                        let _unused = mutex_sensor_manager.lock().unwrap();
717                        spin_sleeper.sleep(sleep_duration_1_second);
718                    }
719                } else {
720                    spin_sleeper.sleep(sleep_duration_1_second);
721                }
722            }
723        }
724        let _ = signal_handler_channels.send_to_sensor_manager(InternalCommand::Quit);
725        signal_handler_channels
726            .receive_from_sensor_manager()
727            .unwrap();
728        let _ = signal_handler_channels.send_to_sensor_manager(InternalCommand::Terminate);
729    }
730
731    fn execute_and_assert_sensor_manager(
732        mut sensor_manager_channels: SensorManagerChannels,
733        mut sensor_manager: SensorManager,
734        mutexes: SensorManagerMutexes,
735        target_value_water_temperature: f32,
736        target_value_ambient_temperature: f32,
737        reference_mutex_access_time_exceeded: bool,
738    ) {
739        let mutexes_clone_for_asserts = mutexes.clone();
740
741        // test object
742        sensor_manager.execute(&mut sensor_manager_channels, mutexes);
743
744        match mutexes_clone_for_asserts
745            .mutex_sensor_manager_signals
746            .lock()
747        {
748            Ok(mutex_sensor_manager_signals_after_run) => {
749                assert_eq!(
750                    mutex_sensor_manager_signals_after_run.water_temperature,
751                    target_value_water_temperature
752                );
753                assert_eq!(
754                    mutex_sensor_manager_signals_after_run.ambient_temperature,
755                    target_value_ambient_temperature
756                );
757            }
758            Err(e) => {
759                panic!("Could not lock mutex: {e:?}")
760            }
761        };
762
763        assert_eq!(
764            sensor_manager.mutex_access_duration_exceeded,
765            reference_mutex_access_time_exceeded
766        );
767    }
768
769    #[test]
770    // Test case verifies if the test object is processing the interfaces correctly using input
771    // from Ds18b20 sensors.
772    pub fn test_sensor_manager_interfaces_using_ds18b20_as_input_without_mutex_blocked() {
773        let channels = Channels::new_for_test();
774
775        let mut config: ConfigData =
776            read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
777        config.sensor_manager.use_simulator = false;
778        config.sensor_manager.source_ambient_temperature = "ds18b20".to_string();
779        config.sensor_manager.source_water_temperature = "ds18b20".to_string();
780
781        let target_value_water_temperature =
782            config.sensor_manager.replacement_value_water_temperature;
783        let target_value_ambient_temperature =
784            config.sensor_manager.replacement_value_ambient_temperature;
785
786        let mutex_ds18b20_water_temperature = Arc::new(Mutex::new(Ok(Ds18b20ResultData::new(
787            target_value_water_temperature,
788        ))));
789        let mutex_ds18b20_ambient_temperature = Arc::new(Mutex::new(Ok(Ds18b20ResultData::new(
790            target_value_ambient_temperature,
791        ))));
792        let mutex_atlas_scientific_temperature = Arc::new(Mutex::new(
793            AtlasScientificResultData::new(target_value_water_temperature + 1.0),
794        ));
795        let mutex_atlas_scientific_conductivity = Arc::new(Mutex::new(
796            AtlasScientificResultData::new(config.sensor_manager.replacement_value_conductivity),
797        ));
798        let mutex_atlas_scientific_ph = Arc::new(Mutex::new(AtlasScientificResultData::new(
799            config.sensor_manager.replacement_value_ph,
800        )));
801        let mutex_sensor_manager_signals = Arc::new(Mutex::new(SensorManagerSignals::new(
802            &config.sensor_manager,
803        )));
804        let dht_result = Ok((target_value_ambient_temperature + 1.0, 2.0));
805        let mutex_dht = Arc::new(Mutex::new(dht_result));
806
807        let sensor_manager = SensorManager::new(config.sensor_manager).unwrap();
808
809        let mutexes = SensorManagerMutexes {
810            mutex_ds18b20_ambient_temperature,
811            mutex_ds18b20_water_temperature,
812            mutex_dht,
813            mutex_atlas_scientific_temperature,
814            mutex_atlas_scientific_ph,
815            mutex_atlas_scientific_conductivity,
816            mutex_sensor_manager_signals,
817        };
818
819        scope(|scope| {
820            scope.spawn(move || {
821                mock_signal_handler_for_sensor_manager(channels.signal_handler, None);
822            });
823
824            scope.spawn(move || {
825                execute_and_assert_sensor_manager(
826                    channels.sensor_manager,
827                    sensor_manager,
828                    mutexes,
829                    target_value_water_temperature,
830                    target_value_ambient_temperature,
831                    false, // no mutex blocking in this test case
832                );
833            });
834        });
835    }
836
837    #[test]
838    // Test case verifies if the test object is processing the interfaces correctly using input
839    // from Ds18b20 sensors.
840    pub fn test_sensor_manager_interfaces_using_atlas_scientific_and_dht_as_input() {
841        let channels = Channels::new_for_test();
842
843        let mut config: ConfigData =
844            read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
845        config.sensor_manager.use_simulator = false;
846        config.sensor_manager.source_ambient_temperature = "dht".to_string();
847        config.sensor_manager.source_water_temperature = "atlas_scientific".to_string();
848
849        let target_value_water_temperature =
850            config.sensor_manager.replacement_value_water_temperature;
851        let target_value_ambient_temperature =
852            config.sensor_manager.replacement_value_ambient_temperature;
853
854        let mutex_ds18b20_water_temperature = Arc::new(Mutex::new(Ok(Ds18b20ResultData::new(
855            target_value_water_temperature + 1.0,
856        ))));
857        let mutex_ds18b20_ambient_temperature = Arc::new(Mutex::new(Ok(Ds18b20ResultData::new(
858            target_value_ambient_temperature + 1.0,
859        ))));
860        let mutex_atlas_scientific_temperature = Arc::new(Mutex::new(
861            AtlasScientificResultData::new(target_value_water_temperature),
862        ));
863        let mutex_atlas_scientific_conductivity = Arc::new(Mutex::new(
864            AtlasScientificResultData::new(config.sensor_manager.replacement_value_conductivity),
865        ));
866        let mutex_atlas_scientific_ph = Arc::new(Mutex::new(AtlasScientificResultData::new(
867            config.sensor_manager.replacement_value_ph,
868        )));
869        let mutex_sensor_manager_signals = Arc::new(Mutex::new(SensorManagerSignals::new(
870            &config.sensor_manager,
871        )));
872        let dht_result = Ok((target_value_ambient_temperature, 2.0));
873        let mutex_dht = Arc::new(Mutex::new(dht_result));
874
875        let sensor_manager = SensorManager::new(config.sensor_manager).unwrap();
876
877        let mutexes = SensorManagerMutexes {
878            mutex_ds18b20_ambient_temperature,
879            mutex_ds18b20_water_temperature,
880            mutex_dht,
881            mutex_atlas_scientific_temperature,
882            mutex_atlas_scientific_ph,
883            mutex_atlas_scientific_conductivity,
884            mutex_sensor_manager_signals,
885        };
886
887        scope(|scope| {
888            scope.spawn(move || {
889                mock_signal_handler_for_sensor_manager(channels.signal_handler, None);
890            });
891
892            scope.spawn(move || {
893                execute_and_assert_sensor_manager(
894                    channels.sensor_manager,
895                    sensor_manager,
896                    mutexes,
897                    target_value_water_temperature,
898                    target_value_ambient_temperature,
899                    false, // no mutex blocking in this test case
900                );
901            });
902        });
903    }
904
905    #[test]
906    // Test case verifies if the test object is processing the interfaces correctly using input
907    // from Ds18b20 sensors while having mutex blocked
908    pub fn test_sensor_manager_interfaces_using_ds18b20_as_input_with_mutex_blocked() {
909        let channels = Channels::new_for_test();
910
911        let mut config: ConfigData =
912            read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
913        config.sensor_manager.use_simulator = false;
914        config.sensor_manager.source_ambient_temperature = "ds18b20".to_string();
915        config.sensor_manager.source_water_temperature = "ds18b20".to_string();
916
917        let target_value_water_temperature =
918            config.sensor_manager.replacement_value_water_temperature;
919        let target_value_ambient_temperature =
920            config.sensor_manager.replacement_value_ambient_temperature;
921
922        let mutex_ds18b20_water_temperature = Arc::new(Mutex::new(Ok(Ds18b20ResultData::new(
923            target_value_water_temperature,
924        ))));
925        let mutex_ds18b20_ambient_temperature = Arc::new(Mutex::new(Ok(Ds18b20ResultData::new(
926            target_value_ambient_temperature,
927        ))));
928        let mutex_atlas_scientific_temperature = Arc::new(Mutex::new(
929            AtlasScientificResultData::new(target_value_water_temperature + 1.0),
930        ));
931        let mutex_atlas_scientific_conductivity = Arc::new(Mutex::new(
932            AtlasScientificResultData::new(config.sensor_manager.replacement_value_conductivity),
933        ));
934        let mutex_atlas_scientific_ph = Arc::new(Mutex::new(AtlasScientificResultData::new(
935            config.sensor_manager.replacement_value_ph,
936        )));
937        let mutex_sensor_manager_signals = Arc::new(Mutex::new(SensorManagerSignals::new(
938            &config.sensor_manager,
939        )));
940        let mutex_sensor_manager_signals_clone_for_test_environment =
941            mutex_sensor_manager_signals.clone();
942
943        let dht_result = Ok((target_value_ambient_temperature + 1.0, 2.0));
944        let mutex_dht = Arc::new(Mutex::new(dht_result));
945
946        let sensor_manager = SensorManager::new(config.sensor_manager).unwrap();
947
948        let mutexes = SensorManagerMutexes {
949            mutex_ds18b20_ambient_temperature,
950            mutex_ds18b20_water_temperature,
951            mutex_dht,
952            mutex_atlas_scientific_temperature,
953            mutex_atlas_scientific_ph,
954            mutex_atlas_scientific_conductivity,
955            mutex_sensor_manager_signals,
956        };
957
958        scope(|scope| {
959            scope.spawn(move || {
960                mock_signal_handler_for_sensor_manager(
961                    channels.signal_handler,
962                    Some(mutex_sensor_manager_signals_clone_for_test_environment),
963                );
964            });
965
966            scope.spawn(move || {
967                execute_and_assert_sensor_manager(
968                    channels.sensor_manager,
969                    sensor_manager,
970                    mutexes,
971                    target_value_water_temperature,
972                    target_value_ambient_temperature,
973                    true, // mutex blocking in this test case
974                );
975            });
976        });
977    }
978}