aquarium_control/food/
food_injection.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
10//! Implements the physical execution of a feed pattern, controlling pumps and the feeder.
11//!
12//! This module provides the `FoodInjectionTrait` and its concrete implementation,
13//! `FoodInjection`. Its core responsibility is to translate a logical `Feedpattern`
14//! into a sequence of physical actions by communicating with the `RelayManager`. It
15//! manages the state of individual actuators, handles precise timing, and allows for
16//! graceful interruption.
17//!
18//! ## Key Components
19//!
20//! - **`FoodInjectionTrait`**: An abstraction for the food injection process. This is a
21//!   critical design element that decouples the main `Feed` controller from the
22//!   concrete implementation, enabling the use of mock injectors for unit testing.
23//!
24//! - **`FoodInjection` Struct**: The primary implementation of the trait. It maintains the
25//!   current known state of all relevant actuators (pumps, skimmer, feeder) to avoid
26//!   sending redundant commands to the `RelayManager`.
27//!
28//! - **`inject_food()` Method**: The main entry point. It iterates through each `FeedPhase`
29//!   of a given `Feedpattern`, orchestrating the complex sequence of pausing pumps,
30//!   running the feeder, and waiting for specified durations.
31//!
32//! - **`switch_skimmer_pumps_feeder()` Method**: A private helper that compares the
33//!   current actuator states with the target states for a given phase. If a state
34//!   mismatch is found, it sends the appropriate `SwitchOn`/`SwitchOff` command and
35//!   waits for confirmation.
36//!
37//! ## Design and Architecture
38//!
39//! The module is designed for robustness and safe operation in a concurrent environment.
40//!
41//! - **Stateful Execution**: By tracking the state of each device (`skimmer_state`,
42//!   `main_pump1_state`, etc.), the `FoodInjection` struct ensures that it only sends
43//!   commands when a state change is actually required.
44//!
45//! - **Interruptible Loops**: The waiting periods within `inject_food` are not simple
46//!   `sleep` calls. They are implemented as tight loops that frequently check for a
47//!   `Quit` command from the `SignalHandler`. This ensures that the entire feeding
48//!   process can be aborted gracefully and quickly at any point.
49//!
50//! - **Guaranteed Cleanup**: Regardless of whether the feed pattern completes, is
51//!   interrupted, or encounters an error, a final cleanup step is always executed.
52//!   This step ensures that all pumps are returned to their active state and the
53//!   feeder is turned off, preventing the system from being left in a hazardous
54//!   or undesirable state.
55//!
56//! - **Comprehensive Error Collection**: Instead of failing on the first error, the
57//!   `inject_food` method collects all errors encountered during the process into a
58//!   `Vec<FoodInjectionError>`. This provides a complete picture of all failures
59//!   that occurred during a single injection attempt, which is invaluable for
60//!   diagnostics.
61
62use spin_sleep::SpinSleeper;
63use std::time::{Duration, Instant};
64
65use crate::food::feed_channels::FeedChannels;
66use crate::food::food_injection_error::FoodInjectionError;
67use crate::food::{feed_config::FeedConfig, feed_pattern::Feedpattern};
68use crate::utilities::channel_content::{ActuatorState, AquariumDevice, InternalCommand};
69use crate::utilities::proc_ext_req::ProcessExternalRequestTrait;
70
71/// Trait for the execution of the feed pattern.
72/// This trait allows running the main control with a mock implementation for testing.
73pub trait FoodInjectionTrait {
74    /// Actuates the feeder according to the specified feed pattern to inject food.
75    ///
76    /// This trait method defines the interface for physically dispensing food based
77    /// on a detailed feed pattern. Implementations will control relevant pumps and
78    /// the feeder motor through communication with a hardware manager (e.g., relay manager),
79    /// while also monitoring for external shutdown commands.
80    ///
81    /// # Arguments
82    /// * `feed_channels` - A mutable reference to the struct containing the channels.
83    /// * `feed_pattern` - A reference to the struct holding the description of the feed pattern.
84    ///
85    /// # Returns
86    /// A tuple `(bool, Result<(), Vec<FoodInjectionError>>)` where:
87    /// - The first element (`bool`) is `true` if a `Quit` command was received from the
88    ///   signal handler, indicating an early termination request. Otherwise, it is `false`.
89    /// - The second element is a `Result`. It is `Ok(())` if the entire sequence is
90    ///   completed without any errors.
91    ///
92    /// # Errors
93    /// The `Result` part of the return tuple will be `Err(Vec<FoodInjectionError>)` if one
94    /// or more errors occurred during the process. The vector will contain all errors
95    /// encountered, which can include:
96    /// - Communication failures with the relay manager (`RelayManagerSend`, `RelayManagerReceive`).
97    /// - An attempt to set a device to an undefined state (e.g., `UndefinedTargetStateSkimmer`).
98    fn inject_food(
99        &mut self,
100        feed_channels: &mut FeedChannels,
101        feedpattern: &Feedpattern,
102    ) -> (bool, Result<(), Vec<FoodInjectionError>>);
103}
104
105/// Struct collects the target actuator states for switch_skimmer_pumps_feeder
106pub struct FoodInjectionTargetActuatorStates {
107    /// target state for protein skimmer
108    target_skimmer_state: ActuatorState,
109
110    /// target state for main pump 1
111    target_main_pump1_state: ActuatorState,
112
113    /// target state for main pump 2
114    target_main_pump2_state: ActuatorState,
115
116    /// target state for aux. pump 1
117    target_aux_pump1_state: ActuatorState,
118
119    /// target state for aux. pump 2
120    target_aux_pump2_state: ActuatorState,
121
122    /// target state for feeder
123    target_feeder_state: ActuatorState,
124}
125
126#[cfg_attr(doc, aquamarine::aquamarine)]
127/// Struct implements the FoodInjectionTrait for executing the feed pattern.
128/// It also contains state attributes for the actuators.
129/// Thread communication is as follows:
130/// ```mermaid
131/// graph LR
132///     food_injection[Food injection] --> relay_manager[Relay Manager]
133///     relay_manager --> food_injection
134///     signal_handler[Signal handler] --> food_injection
135/// ```
136pub struct FoodInjection {
137    skimmer_state: ActuatorState,
138    main_pump1_state: ActuatorState,
139    main_pump2_state: ActuatorState,
140    aux_pump1_state: ActuatorState,
141    aux_pump2_state: ActuatorState,
142    feeder_state: ActuatorState,
143    spin_sleeper: SpinSleeper,
144    sleep_duration_device_switch: Duration,
145}
146
147impl FoodInjection {
148    /// Creates a new `FoodInjection` instance.
149    ///
150    /// This constructor initializes the food injection control module. It sets
151    /// the initial state of all associated actuators (skimmer, pumps, feeder)
152    /// to `Undefined` and configures internal timing mechanisms, such as the
153    /// delay between device switches, based on the provided `FeedConfig`.
154    ///
155    /// # Arguments
156    /// * `config` - A reference to the `FeedConfig` struct, which contains
157    ///   parameters like `device_switch_delay_millis`.
158    ///
159    /// # Returns
160    /// A new `FoodInjection` struct, ready to execute food dispensing operations.
161    pub fn new(config: &FeedConfig) -> FoodInjection {
162        FoodInjection {
163            skimmer_state: ActuatorState::Undefined,
164            main_pump1_state: ActuatorState::Undefined,
165            main_pump2_state: ActuatorState::Undefined,
166            aux_pump1_state: ActuatorState::Undefined,
167            aux_pump2_state: ActuatorState::Undefined,
168            feeder_state: ActuatorState::Undefined,
169            spin_sleeper: SpinSleeper::default(),
170            sleep_duration_device_switch: Duration::from_millis(
171                config.device_switch_delay_millis as u64,
172            ),
173        }
174    }
175
176    /// Commands the specified aquarium actuators (skimmer, main pumps, auxiliary pumps, feeder)
177    /// to switch to their target states.
178    ///
179    /// This private helper function iterates through a set of target states for various devices.
180    /// For each device whose current state differs from its target state, it sends an
181    /// appropriate `SwitchOn` or `SwitchOff` command to the relay manager
182    /// and waits for an acknowledgment. It includes a small delay between each switch command.
183    ///
184    /// # Arguments
185    /// * `target_actuator_states` - A `FoodInjectionTargetActuatorStates` struct containing the
186    ///   desired `ActuatorState` for each relevant device.
187    /// * `feed_channels` - A mutable reference to the struct containing the channels.
188    ///
189    /// # Returns
190    /// An `Ok(())` if all devices were switched successfully.
191    ///
192    /// # Errors
193    /// Returns an `Err(Vec<FoodInjectionError>)` containing a list of all errors that
194    /// occurred during the switching process. This function attempts to switch all devices
195    /// even if some fail. Possible errors include:
196    /// - `RelayManagerSend`: Failure to send a command to the relay manager's channel.
197    /// - `RelayManagerReceive`: Failure to receive an acknowledgment from the relay manager.
198    /// - `UndefinedTargetState...`: An attempt was made to set a device to a state other
199    ///   than `On` or `Off`.
200    fn switch_skimmer_pumps_feeder(
201        &mut self,
202        target_actuator_states: FoodInjectionTargetActuatorStates,
203        feed_channels: &mut FeedChannels,
204    ) -> Result<(), Vec<FoodInjectionError>> {
205        // Initialize a vector to collect any errors that occur.
206        let mut errors: Vec<FoodInjectionError> = Vec::new();
207
208        // --- Skimmer ---
209        if self.skimmer_state != target_actuator_states.target_skimmer_state {
210            self.spin_sleeper.sleep(self.sleep_duration_device_switch);
211            let device = AquariumDevice::Skimmer;
212
213            match target_actuator_states.target_skimmer_state {
214                ActuatorState::On | ActuatorState::Off => {
215                    // Command is defined
216                    let command =
217                        if target_actuator_states.target_skimmer_state == ActuatorState::On {
218                            InternalCommand::SwitchOn(device.clone())
219                        } else {
220                            InternalCommand::SwitchOff(device.clone())
221                        };
222                    if let Err(e) = feed_channels.send_to_relay_manager(command) {
223                        #[cfg(test)]
224                        println!(
225                            "Encountered error when sending command {:?} for {} to relay manager: {e:?}",
226                            target_actuator_states.target_skimmer_state,
227                            AquariumDevice::Skimmer
228                        );
229                        errors.push(FoodInjectionError::RelayManagerSend {
230                            location: module_path!().to_string(),
231                            device: device.clone(),
232                            source: e,
233                        });
234                    } else if let Err(e) = feed_channels.receive_from_relay_manager() {
235                        println!(
236                            "Encountered error when receiving command to relay manager: {e:?}"
237                        );
238                        errors.push(FoodInjectionError::RelayManagerReceive {
239                            location: module_path!().to_string(),
240                            device: device.clone(),
241                            source: e,
242                        });
243                    } else {
244                        // Only update state on success
245                        println!(
246                            "Successfully set skimmer state to {:?}",
247                            target_actuator_states.target_skimmer_state
248                        );
249                        self.skimmer_state = target_actuator_states.target_skimmer_state;
250                    }
251                }
252                // Handle undefined state by pushing a specific error.
253                _ => errors.push(FoodInjectionError::UndefinedTargetState(
254                    module_path!().to_string(),
255                    AquariumDevice::Skimmer,
256                )),
257            }
258        }
259
260        // --- Main pump 1 ---
261        if self.main_pump1_state != target_actuator_states.target_main_pump1_state {
262            self.spin_sleeper.sleep(self.sleep_duration_device_switch);
263            let device = AquariumDevice::MainPump1;
264
265            match target_actuator_states.target_main_pump1_state {
266                ActuatorState::On | ActuatorState::Off => {
267                    // Command is defined
268                    let command =
269                        if target_actuator_states.target_main_pump1_state == ActuatorState::On {
270                            InternalCommand::SwitchOn(device.clone())
271                        } else {
272                            InternalCommand::SwitchOff(device.clone())
273                        };
274                    if let Err(e) = feed_channels.send_to_relay_manager(command) {
275                        #[cfg(test)]
276                        println!(
277                            "Encountered error when sending command {:?} for {} to relay manager: {e:?}",
278                            target_actuator_states.target_main_pump1_state,
279                            AquariumDevice::MainPump1
280                        );
281                        errors.push(FoodInjectionError::RelayManagerSend {
282                            location: module_path!().to_string(),
283                            device: device.clone(),
284                            source: e,
285                        });
286                    } else if let Err(e) = feed_channels.receive_from_relay_manager() {
287                        errors.push(FoodInjectionError::RelayManagerReceive {
288                            location: module_path!().to_string(),
289                            device: device.clone(),
290                            source: e,
291                        });
292                    } else {
293                        // Only update state on success
294                        self.main_pump1_state = target_actuator_states.target_main_pump1_state;
295                    }
296                }
297                // Handle undefined state by pushing a specific error.
298                _ => errors.push(FoodInjectionError::UndefinedTargetState(
299                    module_path!().to_string(),
300                    AquariumDevice::MainPump1,
301                )),
302            }
303        }
304
305        // --- Main pump 2 ---
306        if self.main_pump2_state != target_actuator_states.target_main_pump2_state {
307            self.spin_sleeper.sleep(self.sleep_duration_device_switch);
308            let device = AquariumDevice::MainPump2;
309
310            match target_actuator_states.target_main_pump2_state {
311                ActuatorState::On | ActuatorState::Off => {
312                    // Command is defined
313                    let command =
314                        if target_actuator_states.target_main_pump2_state == ActuatorState::On {
315                            InternalCommand::SwitchOn(device.clone())
316                        } else {
317                            InternalCommand::SwitchOff(device.clone())
318                        };
319                    if let Err(e) = feed_channels.send_to_relay_manager(command) {
320                        #[cfg(test)]
321                        println!(
322                            "Encountered error when sending command {:?} for {} to relay manager: {e:?}",
323                            target_actuator_states.target_main_pump2_state,
324                            AquariumDevice::MainPump2
325                        );
326                        errors.push(FoodInjectionError::RelayManagerSend {
327                            location: module_path!().to_string(),
328                            device: device.clone(),
329                            source: e,
330                        });
331                    } else if let Err(e) = feed_channels.receive_from_relay_manager() {
332                        errors.push(FoodInjectionError::RelayManagerReceive {
333                            location: module_path!().to_string(),
334                            device: device.clone(),
335                            source: e,
336                        });
337                    } else {
338                        // Only update state on success
339                        self.main_pump2_state = target_actuator_states.target_main_pump2_state;
340                    }
341                }
342                // Handle undefined state by pushing a specific error.
343                _ => errors.push(FoodInjectionError::UndefinedTargetState(
344                    module_path!().to_string(),
345                    AquariumDevice::MainPump2,
346                )),
347            }
348        }
349
350        // --- Aux. pump 1 ---
351        if self.aux_pump1_state != target_actuator_states.target_aux_pump1_state {
352            self.spin_sleeper.sleep(self.sleep_duration_device_switch);
353            let device = AquariumDevice::AuxPump1;
354
355            match target_actuator_states.target_aux_pump1_state {
356                ActuatorState::On | ActuatorState::Off => {
357                    // Command is defined
358                    let command =
359                        if target_actuator_states.target_aux_pump1_state == ActuatorState::On {
360                            InternalCommand::SwitchOn(device.clone())
361                        } else {
362                            InternalCommand::SwitchOff(device.clone())
363                        };
364                    if let Err(e) = feed_channels.send_to_relay_manager(command) {
365                        #[cfg(test)]
366                        println!(
367                            "Encountered error when sending command {:?} for {} to relay manager: {e:?}",
368                            target_actuator_states.target_aux_pump1_state,
369                            AquariumDevice::AuxPump1
370                        );
371                        errors.push(FoodInjectionError::RelayManagerSend {
372                            location: module_path!().to_string(),
373                            device: device.clone(),
374                            source: e,
375                        });
376                    } else if let Err(e) = feed_channels.receive_from_relay_manager() {
377                        errors.push(FoodInjectionError::RelayManagerReceive {
378                            location: module_path!().to_string(),
379                            device: device.clone(),
380                            source: e,
381                        });
382                    } else {
383                        // Only update state on success
384                        self.aux_pump1_state = target_actuator_states.target_aux_pump1_state;
385                    }
386                }
387                // Handle undefined state by pushing a specific error.
388                _ => errors.push(FoodInjectionError::UndefinedTargetState(
389                    module_path!().to_string(),
390                    AquariumDevice::AuxPump1,
391                )),
392            }
393        }
394
395        // --- Aux. pump 2 ---
396        if self.aux_pump2_state != target_actuator_states.target_aux_pump2_state {
397            self.spin_sleeper.sleep(self.sleep_duration_device_switch);
398            let device = AquariumDevice::AuxPump2;
399
400            match target_actuator_states.target_aux_pump2_state {
401                ActuatorState::On | ActuatorState::Off => {
402                    // Command is defined
403                    let command =
404                        if target_actuator_states.target_aux_pump2_state == ActuatorState::On {
405                            InternalCommand::SwitchOn(device.clone())
406                        } else {
407                            InternalCommand::SwitchOff(device.clone())
408                        };
409                    if let Err(e) = feed_channels.send_to_relay_manager(command) {
410                        #[cfg(test)]
411                        println!(
412                            "Encountered error when sending command {:?} for {} to relay manager: {e:?}",
413                            target_actuator_states.target_aux_pump2_state,
414                            AquariumDevice::AuxPump2
415                        );
416                        errors.push(FoodInjectionError::RelayManagerSend {
417                            location: module_path!().to_string(),
418                            device: device.clone(),
419                            source: e,
420                        });
421                    } else if let Err(e) = feed_channels.receive_from_relay_manager() {
422                        errors.push(FoodInjectionError::RelayManagerReceive {
423                            location: module_path!().to_string(),
424                            device: device.clone(),
425                            source: e,
426                        });
427                    } else {
428                        // Only update state on success
429                        self.aux_pump2_state = target_actuator_states.target_aux_pump2_state;
430                    }
431                }
432                // Handle undefined state by pushing a specific error.
433                _ => errors.push(FoodInjectionError::UndefinedTargetState(
434                    module_path!().to_string(),
435                    AquariumDevice::AuxPump2,
436                )),
437            }
438        }
439
440        // --- Feeder ---
441        if self.feeder_state != target_actuator_states.target_feeder_state {
442            self.spin_sleeper.sleep(self.sleep_duration_device_switch);
443            let device = AquariumDevice::Feeder;
444
445            match target_actuator_states.target_feeder_state {
446                ActuatorState::On | ActuatorState::Off => {
447                    // Command is defined
448                    let command = if target_actuator_states.target_feeder_state == ActuatorState::On
449                    {
450                        InternalCommand::SwitchOn(device.clone())
451                    } else {
452                        InternalCommand::SwitchOff(device.clone())
453                    };
454                    if let Err(e) = feed_channels.send_to_relay_manager(command) {
455                        #[cfg(test)]
456                        println!(
457                            "Encountered error when sending command {:?} for {} to relay manager: {e:?}",
458                            target_actuator_states.target_feeder_state,
459                            AquariumDevice::Feeder
460                        );
461                        errors.push(FoodInjectionError::RelayManagerSend {
462                            location: module_path!().to_string(),
463                            device: device.clone(),
464                            source: e,
465                        });
466                    } else if let Err(e) = feed_channels.receive_from_relay_manager() {
467                        errors.push(FoodInjectionError::RelayManagerReceive {
468                            location: module_path!().to_string(),
469                            device: device.clone(),
470                            source: e,
471                        });
472                    } else {
473                        // Only update state on success
474                        self.feeder_state = target_actuator_states.target_feeder_state;
475                    }
476                }
477                // Handle undefined state by pushing a specific error.
478                _ => errors.push(FoodInjectionError::UndefinedTargetState(
479                    module_path!().to_string(),
480                    AquariumDevice::Feeder,
481                )),
482            }
483        }
484
485        if errors.is_empty() {
486            Ok(())
487        } else {
488            Err(errors)
489        }
490    }
491}
492
493impl FoodInjectionTrait for FoodInjection {
494    /// Actuates the feeder according to the specified feed pattern to inject food.
495    ///
496    /// This implementation executes a sequence of feed phases defined in the `feedpattern`.
497    /// For each phase, it sets the state of various pumps and the feeder, waits for a
498    /// specified duration, and continuously checks for a `Quit` command from the signal
499    /// handler to allow for graceful interruption.
500    ///
501    /// After the sequence is complete or aborted, it performs a cleanup step to ensure all
502    /// pumps are returned to an active state and the feeder is off.
503    ///
504    /// # Arguments
505    /// * `feed_channels` - A mutable reference to the struct containing the channels.
506    /// * `feedpattern` - A reference to the struct holding the description of the feed pattern.
507    ///
508    /// # Returns
509    /// A tuple `(bool, Result<(), Vec<FoodInjectionError>>)` where:
510    /// - The first element (`bool`) is `true` if a `Quit` command was received from the
511    ///   signal handler, indicating an early termination request. Otherwise, it is `false`.
512    /// - The second element is a `Result`. It is `Ok(())` if the entire sequence is
513    ///   completed without any errors.
514    ///
515    /// # Errors
516    /// The `Result` part of the return tuple will be `Err(Vec<FoodInjectionError>)` if one
517    /// or more errors occurred during the process. The function aborts the feed sequence
518    /// on the first error but still performs the final cleanup step. The vector will
519    /// contain all errors encountered during both the main sequence and the cleanup.
520    fn inject_food(
521        &mut self,
522        feed_channels: &mut FeedChannels,
523        feedpattern: &Feedpattern,
524    ) -> (bool, Result<(), Vec<FoodInjectionError>>) {
525        let spin_sleeper = SpinSleeper::default();
526        let sleep_interval_millis: i16 = 1;
527        let sleep_duration = Duration::from_millis(sleep_interval_millis as u64);
528        let mut target_skimmer_state: ActuatorState;
529        let mut target_main_pump1_state: ActuatorState;
530        let mut target_main_pump2_state: ActuatorState;
531        let mut target_aux_pump1_state: ActuatorState;
532        let mut target_aux_pump2_state: ActuatorState;
533        let mut quit_command_received: bool = false;
534        let mut collected_errors_cumulated: Vec<FoodInjectionError> = vec![];
535
536        for feedphase in &feedpattern.feedphases {
537            // check if there is a quit request
538            (quit_command_received, _, _) =
539                self.process_external_request(&mut feed_channels.rx_feed_from_signal_handler, None);
540            if quit_command_received {
541                break; // exit outer loop
542            }
543
544            // keep feeder stopped
545            if feedphase.pause_duration > 0 {
546                target_skimmer_state = match feedphase.pause_skimmer {
547                    true => ActuatorState::On,
548                    false => ActuatorState::Off,
549                };
550                target_main_pump1_state = match feedphase.pause_main_pump_1 {
551                    true => ActuatorState::On,
552                    false => ActuatorState::Off,
553                };
554                target_main_pump2_state = match feedphase.pause_main_pump_2 {
555                    true => ActuatorState::On,
556                    false => ActuatorState::Off,
557                };
558                target_aux_pump1_state = match feedphase.pause_aux_pump_1 {
559                    true => ActuatorState::On,
560                    false => ActuatorState::Off,
561                };
562                target_aux_pump2_state = match feedphase.pause_aux_pump_2 {
563                    true => ActuatorState::On,
564                    false => ActuatorState::Off,
565                };
566                let target_actuator_states = FoodInjectionTargetActuatorStates {
567                    target_skimmer_state,
568                    target_main_pump1_state,
569                    target_main_pump2_state,
570                    target_aux_pump1_state,
571                    target_aux_pump2_state,
572                    target_feeder_state: ActuatorState::Off,
573                };
574                if let Err(collected_errors) =
575                    self.switch_skimmer_pumps_feeder(target_actuator_states, feed_channels)
576                {
577                    collected_errors_cumulated.extend(collected_errors);
578                    break; // if any error occurs, abort the feed sequence
579                }
580            }
581
582            // wait for a determined period and check quit request in between
583            let start_of_pause = Instant::now();
584            let pause_duration = Duration::from_millis(feedphase.pause_duration as u64);
585            while Instant::now().duration_since(start_of_pause) < pause_duration {
586                (quit_command_received, _, _) = self
587                    .process_external_request(&mut feed_channels.rx_feed_from_signal_handler, None);
588                if quit_command_received {
589                    #[cfg(test)]
590                    println!("Received quit command during pause phase of food injection");
591                    break; // exit inner loop
592                }
593                spin_sleeper.sleep(sleep_duration);
594            }
595            if quit_command_received {
596                break; // exit outer loop
597            }
598
599            // run the feeder
600            if feedphase.feed_duration > 0 {
601                target_skimmer_state = match feedphase.feed_skimmer {
602                    true => ActuatorState::On,
603                    false => ActuatorState::Off,
604                };
605                target_main_pump1_state = match feedphase.feed_main_pump_1 {
606                    true => ActuatorState::On,
607                    false => ActuatorState::Off,
608                };
609                target_main_pump2_state = match feedphase.feed_main_pump_2 {
610                    true => ActuatorState::On,
611                    false => ActuatorState::Off,
612                };
613                target_aux_pump1_state = match feedphase.feed_aux_pump_1 {
614                    true => ActuatorState::On,
615                    false => ActuatorState::Off,
616                };
617                target_aux_pump2_state = match feedphase.feed_aux_pump_2 {
618                    true => ActuatorState::On,
619                    false => ActuatorState::Off,
620                };
621                let target_actuator_states = FoodInjectionTargetActuatorStates {
622                    target_skimmer_state,
623                    target_main_pump1_state,
624                    target_main_pump2_state,
625                    target_aux_pump1_state,
626                    target_aux_pump2_state,
627                    target_feeder_state: ActuatorState::On,
628                };
629                if let Err(collected_errors) =
630                    self.switch_skimmer_pumps_feeder(target_actuator_states, feed_channels)
631                {
632                    collected_errors_cumulated.extend(collected_errors);
633                    break; // if any error occurs, abort the feed sequence
634                }
635            }
636
637            // wait for a determined period and check quit request in between
638            let start_of_pause = Instant::now();
639            let feed_duration = Duration::from_millis(feedphase.feed_duration as u64);
640            while Instant::now().duration_since(start_of_pause) < feed_duration {
641                (quit_command_received, _, _) = self
642                    .process_external_request(&mut feed_channels.rx_feed_from_signal_handler, None);
643                if quit_command_received {
644                    #[cfg(test)]
645                    println!("Received quit command during feed phase of food injection");
646                    break; // exit inner loop
647                }
648                spin_sleeper.sleep(sleep_duration);
649            }
650            if quit_command_received {
651                break; // exit outer loop
652            }
653        }
654
655        // The feed sequence has finished or has been aborted.
656        // Switch on all pumps. Switch off feeder.
657        if let Err(collected_errors) = self.switch_skimmer_pumps_feeder(
658            FoodInjectionTargetActuatorStates {
659                target_skimmer_state: ActuatorState::On,
660                target_main_pump1_state: ActuatorState::On,
661                target_main_pump2_state: ActuatorState::On,
662                target_aux_pump1_state: ActuatorState::On,
663                target_aux_pump2_state: ActuatorState::On,
664                target_feeder_state: ActuatorState::Off,
665            },
666            feed_channels,
667        ) {
668            collected_errors_cumulated.extend(collected_errors);
669        }
670
671        if collected_errors_cumulated.is_empty() {
672            (quit_command_received, Ok(()))
673        } else {
674            (quit_command_received, Err(collected_errors_cumulated))
675        }
676    }
677}
678
679#[cfg(test)]
680pub mod tests {
681    use crate::food::feed_pattern::{FeedPhase, Feedpattern};
682    use crate::food::food_injection::{FoodInjection, FoodInjectionTrait};
683    use crate::launch::channels::{AquaReceiver, AquaSender, Channels};
684    use crate::utilities::channel_content::{ActuatorState, AquariumDevice, InternalCommand};
685    use crate::utilities::config::{read_config_file, ConfigData};
686    use all_asserts::{assert_ge, assert_le};
687    use std::thread::scope;
688    use std::time::Instant;
689
690    // Simulates the relay manager's behavior in response to actuation commands.
691    //
692    // This helper function is used within test scopes to act as a mock for the relay manager.
693    // It waits to receive a `SwitchOn` or `SwitchOff` command from the
694    // `FoodInjection` test object, prints it, sends a confirmation back, and increments
695    // counters for the number of on/off commands received.
696    //
697    // # Arguments
698    // * `tx_relay_manager_to_feed` - The sender channel for this mock to send acknowledgments back.
699    // * `rx_relay_manager_from_feed` - The receiver channel for this mock to get actuation commands.
700    // * `counter_switch_on` - A mutable counter to track the number of `SwitchOn` commands received.
701    // * `counter_switch_off` - A mutable counter to track the number of `SwitchOff` commands received.
702    //
703    // # Returns
704    // The `InternalCommand` that was received from the test object.
705    //
706    // # Panics
707    // This function will panic if:
708    // - It fails to receive a command from the `FoodInjection` test object.
709    // - It receives an `InternalCommand` that is neither `SwitchOn` nor `SwitchOff`.
710    // - It fails to send the acknowledgment back to the `FoodInjection` test object.
711    fn test_food_injection_receive_actuation_command_send_confirmation(
712        tx_relay_manager_to_feed: &mut AquaSender<bool>,
713        rx_relay_manager_from_feed: &mut AquaReceiver<InternalCommand>,
714        counter_switch_on: &mut i32,
715        counter_switch_off: &mut i32,
716    ) -> InternalCommand {
717        // receive command from the test object
718        let (command, device) = match rx_relay_manager_from_feed.recv() {
719            Ok(c) => match c {
720                InternalCommand::SwitchOn(ref d) => (c.clone(), d.clone()),
721                InternalCommand::SwitchOff(ref d) => (c.clone(), d.clone()),
722                _ => {
723                    panic!("FoodInjection: Received invalid command from test object:");
724                }
725            },
726            Err(e) => {
727                panic!(
728                    "FoodInjection: Error when receiving actuation command from test object: {e:?}"
729                );
730            }
731        };
732
733        println!(
734            "Received command {} for {} from test object",
735            command, device
736        );
737
738        // send confirmation back
739        match tx_relay_manager_to_feed.send(true) {
740            Ok(_) => {}
741            Err(e) => {
742                panic!("FoodInjection: Error when sending back actuation command confirmation to test object: {e:?}");
743            }
744        }
745
746        match command {
747            InternalCommand::SwitchOn(_) => {
748                *counter_switch_on += 1;
749            }
750            InternalCommand::SwitchOff(_) => {
751                *counter_switch_off += 1;
752            }
753            _ => {
754                // Do nothing. This case is covered with panic beforehand.
755            }
756        }
757        command
758    }
759
760    // Helper function to record the actuation state of the devices based on a command.
761    //
762    // This test helper takes a command and updates the state of the corresponding
763    // actuator in the provided mutable state variables. This allows tests to track
764    // the expected state of the system.
765    //
766    // # Arguments
767    // * `command` - The `InternalCommand` (`SwitchOn` or `SwitchOff`) to process.
768    // * `..._actuation_state` - Mutable references to the state variables for each device.
769    fn update_actuator_states(
770        command: InternalCommand,
771        skimmer_actuation_state: &mut ActuatorState,
772        main_pump1_actuation_state: &mut ActuatorState,
773        main_pump2_actuation_state: &mut ActuatorState,
774        aux_pump1_actuation_state: &mut ActuatorState,
775        aux_pump2_actuation_state: &mut ActuatorState,
776        feeder_actuation_state: &mut ActuatorState,
777    ) {
778        // Determine the new state and the target device from the command.
779        let (device, new_state) = match command {
780            InternalCommand::SwitchOn(device) => (device, ActuatorState::On),
781            InternalCommand::SwitchOff(device) => (device, ActuatorState::Off),
782            // If it's any other command, we don't need to do anything.
783            _ => return,
784        };
785
786        // Update the state of the correct pump in a single match block.
787        match device {
788            AquariumDevice::Skimmer => {
789                *skimmer_actuation_state = new_state;
790            }
791            AquariumDevice::MainPump1 => {
792                *main_pump1_actuation_state = new_state;
793            }
794            AquariumDevice::MainPump2 => {
795                *main_pump2_actuation_state = new_state;
796            }
797            AquariumDevice::AuxPump1 => {
798                *aux_pump1_actuation_state = new_state;
799            }
800            AquariumDevice::AuxPump2 => {
801                *aux_pump2_actuation_state = new_state;
802            }
803            AquariumDevice::Feeder => {
804                *feeder_actuation_state = new_state;
805            }
806            // Ignore commands for any other devices.
807            _ => {}
808        }
809    }
810
811    #[test]
812    // this test case executes the food injection without interruption from the signal handler
813    pub fn test_food_injection_uninterrupted() {
814        let config: ConfigData =
815            read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
816        let mut food_injection = FoodInjection::new(&config.feed);
817
818        let mut channels = Channels::new_for_test();
819
820        scope(|scope| {
821            // this scope is providing the test environment
822            scope.spawn(move || {
823                let mut counter_switch_on: i32 = 0;
824                let mut counter_switch_off: i32 = 0;
825                let mut skimmer_actuation_state = ActuatorState::On;
826                let mut main_pump1_actuation_state = ActuatorState::On;
827                let mut main_pump2_actuation_state = ActuatorState::On;
828                let mut aux_pump1_actuation_state = ActuatorState::On;
829                let mut aux_pump2_actuation_state = ActuatorState::On;
830                let mut feeder_actuation_state = ActuatorState::Off;
831
832                for i in 0..126 {
833                    println!("test_food_injection: loop #{}", i);
834                    let command = test_food_injection_receive_actuation_command_send_confirmation(
835                        &mut channels.relay_manager.tx_relay_manager_to_feed,
836                        &mut channels.relay_manager.rx_relay_manager_from_feed,
837                        &mut counter_switch_on,
838                        &mut counter_switch_off,
839                    );
840                    update_actuator_states(
841                        command,
842                        &mut skimmer_actuation_state,
843                        &mut main_pump1_actuation_state,
844                        &mut main_pump2_actuation_state,
845                        &mut aux_pump1_actuation_state,
846                        &mut aux_pump2_actuation_state,
847                        &mut feeder_actuation_state,
848                    );
849                }
850                println!(
851                    "counter_switch_on={} counter_switch_off={}",
852                    counter_switch_on, counter_switch_off
853                );
854                assert_eq!(counter_switch_on, 65);
855                assert_eq!(counter_switch_off, 61);
856                assert_eq!(skimmer_actuation_state, ActuatorState::On);
857                assert_eq!(main_pump1_actuation_state, ActuatorState::On);
858                assert_eq!(main_pump2_actuation_state, ActuatorState::On);
859                assert_eq!(aux_pump1_actuation_state, ActuatorState::On);
860                assert_eq!(aux_pump2_actuation_state, ActuatorState::On);
861                assert_eq!(feeder_actuation_state, ActuatorState::Off);
862            });
863
864            // this scope is executing the test object
865            scope.spawn(move || {
866                let feedphase = FeedPhase::new(
867                    200, true, true, true, true, true, 200, false, false, false, false, false,
868                );
869                // Each phase will trigger switching on 6 devices and switching off 6 devices
870                let mut feedphases: Vec<FeedPhase> = Vec::new();
871                feedphases.push(feedphase.clone());
872                feedphases.push(feedphase.clone());
873                feedphases.push(feedphase.clone());
874                feedphases.push(feedphase.clone());
875                feedphases.push(feedphase.clone());
876                feedphases.push(feedphase.clone());
877                feedphases.push(feedphase.clone());
878                feedphases.push(feedphase.clone());
879                feedphases.push(feedphase.clone());
880                feedphases.push(feedphase.clone());
881
882                // After all phases are executed, the feeder will be switched off
883                // and 5 devices will be switched on
884
885                let feedpattern = Feedpattern {
886                    profile_id: 1,
887                    profile_name: "Test1".to_string(),
888                    feedphases,
889                };
890
891                let start_time = Instant::now();
892                let result_tuple = food_injection.inject_food(&mut channels.feed, &feedpattern);
893                let end_time = Instant::now();
894
895                let execution_duration = end_time - start_time;
896
897                println!(
898                    "execution_duration (milliseconds)= {}",
899                    execution_duration.as_millis()
900                );
901
902                let (interrupted, result) = result_tuple;
903                assert_eq!(interrupted, false);
904                assert!(result.is_ok());
905
906                assert_ge!(execution_duration.as_millis(), 22500);
907                assert_le!(execution_duration.as_millis(), 29700);
908            });
909        });
910    }
911
912    #[test]
913    // this test case executes the food injection with interruption from the signal handler
914    pub fn test_food_injection_interrupted() {
915        let config: ConfigData =
916            read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
917        let mut food_injection = FoodInjection::new(&config.feed);
918
919        let mut channels = Channels::new_for_test();
920
921        scope(|scope| {
922            // this scope is providing the test environment
923            scope.spawn(move || {
924                let mut counter_switch_on: i32 = 0;
925                let mut counter_switch_off: i32 = 0;
926                let mut skimmer_actuation_state = ActuatorState::On;
927                let mut main_pump1_actuation_state = ActuatorState::On;
928                let mut main_pump2_actuation_state = ActuatorState::On;
929                let mut aux_pump1_actuation_state = ActuatorState::On;
930                let mut aux_pump2_actuation_state = ActuatorState::On;
931                let mut feeder_actuation_state = ActuatorState::Off;
932
933                for i in 0..5 {
934                    println!("test_food_injection: loop #{}", i);
935                    let command = test_food_injection_receive_actuation_command_send_confirmation(
936                        &mut channels.relay_manager.tx_relay_manager_to_feed,
937                        &mut channels.relay_manager.rx_relay_manager_from_feed,
938                        &mut counter_switch_on,
939                        &mut counter_switch_off,
940                    );
941                    update_actuator_states(
942                        command,
943                        &mut skimmer_actuation_state,
944                        &mut main_pump1_actuation_state,
945                        &mut main_pump2_actuation_state,
946                        &mut aux_pump1_actuation_state,
947                        &mut aux_pump2_actuation_state,
948                        &mut feeder_actuation_state,
949                    );
950                }
951                println!("test_food_injection_interrupted: sending Quit signal");
952                _ = channels.signal_handler.send_to_feed(InternalCommand::Quit);
953                for i in 0..1 {
954                    println!("test_food_injection: loop #{}", i);
955                    let command = test_food_injection_receive_actuation_command_send_confirmation(
956                        &mut channels.relay_manager.tx_relay_manager_to_feed,
957                        &mut channels.relay_manager.rx_relay_manager_from_feed,
958                        &mut counter_switch_on,
959                        &mut counter_switch_off,
960                    );
961                    update_actuator_states(
962                        command,
963                        &mut skimmer_actuation_state,
964                        &mut main_pump1_actuation_state,
965                        &mut main_pump2_actuation_state,
966                        &mut aux_pump1_actuation_state,
967                        &mut aux_pump2_actuation_state,
968                        &mut feeder_actuation_state,
969                    );
970                }
971                println!(
972                    "counter_switch_on={} counter_switch_off={}",
973                    counter_switch_on, counter_switch_off
974                );
975                assert_eq!(counter_switch_on, 5);
976                assert_eq!(counter_switch_off, 1);
977                assert_eq!(skimmer_actuation_state, ActuatorState::On);
978                assert_eq!(main_pump1_actuation_state, ActuatorState::On);
979                assert_eq!(main_pump2_actuation_state, ActuatorState::On);
980                assert_eq!(aux_pump1_actuation_state, ActuatorState::On);
981                assert_eq!(aux_pump2_actuation_state, ActuatorState::On);
982                assert_eq!(feeder_actuation_state, ActuatorState::Off);
983            });
984
985            // this scope is executing the test object
986            scope.spawn(move || {
987                let feedphase = FeedPhase::new(
988                    200, true, true, true, true, true, 200, false, false, false, false, false,
989                );
990                // Each phase will trigger switching on 6 devices and switching off 6 devices
991                let mut feedphases: Vec<FeedPhase> = Vec::new();
992                feedphases.push(feedphase.clone());
993                feedphases.push(feedphase.clone());
994                feedphases.push(feedphase.clone());
995                feedphases.push(feedphase.clone());
996                feedphases.push(feedphase.clone());
997                feedphases.push(feedphase.clone());
998                feedphases.push(feedphase.clone());
999                feedphases.push(feedphase.clone());
1000                feedphases.push(feedphase.clone());
1001                feedphases.push(feedphase.clone());
1002
1003                // After all phases are executed, the feeder will be switched off
1004                // and 5 devices will be switched on
1005
1006                let feedpattern = Feedpattern {
1007                    profile_id: 1,
1008                    profile_name: "Test1".to_string(),
1009                    feedphases,
1010                };
1011
1012                let start_time = Instant::now();
1013                let result_tuple = food_injection.inject_food(&mut channels.feed, &feedpattern);
1014                let end_time = Instant::now();
1015
1016                let execution_duration = end_time - start_time;
1017                let (interrupted, result) = result_tuple;
1018
1019                println!(
1020                    "execution_duration (milliseconds)= {}",
1021                    execution_duration.as_millis()
1022                );
1023                if let Err(error_vector) = result.clone() {
1024                    println!("result contains error(s):",);
1025                    for e in error_vector {
1026                        println!("{:?}", e);
1027                    }
1028                }
1029
1030                assert_eq!(interrupted, true);
1031                assert!(result.is_ok());
1032                assert_ge!(execution_duration.as_millis(), 850);
1033                assert_le!(execution_duration.as_millis(), 1085);
1034            });
1035        });
1036    }
1037}