aquarium_control/food/
feed_pattern.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//! Defines the data structures that represent a complete, multistep feeding process.
11//!
12//! This module provides the `Feedpattern` and `FeedPhase` structs, which together describe
13//! a single, executable feeding profile. These structures are typically populated with data
14//! retrieved from the database and are used by the `Feed` controller to orchestrate the
15//! feeding sequence.
16//!
17//! ## Key Components
18//!
19//! - **`Feedpattern`**: Represents a complete feeding profile. It consists of a profile name
20//!   and a vector of one or more `FeedPhase`s. It acts as a container for the entire
21//!   sequence of actions.
22//!
23//! - **`FeedPhase`**: Represents a single step within a `Feedpattern`. Each phase is composed
24//!   of two parts:
25//!   1.  A **pause** period, during which certain pumps can be temporarily disabled.
26//!   2.  A **feed** period, where the feeder motor is activated for a specific duration.
27//!
28//!   The state (on or off) of the skimmer and various pumps can be configured independently
29//!   for both the pause and feed parts of a phase.
30//!
31//! ## Design and Architecture
32//!
33//! The design allows for complex and flexible feeding schedules. A simple feeding might
34//! consist of a single `FeedPhase` (e.g., "pause pumps, run feeder, resume pumps"). A more
35//! complex pattern could involve multiple phases to dispense multiple quantities of food or
36//! to allow food to circulate before resuming full filtration.
37//!
38//! ### Example of a Multi-Phase Pattern
39//!
40//! A `Feedpattern` could be configured to:
41//! 1.  **Phase 1**: Pause the main pumps and skimmer, then run the feeder for 2 seconds to dispense food.
42//! 2.  **Phase 2**: Keep pumps off for a 10-second pause to let fish eat, with no feeder activity.
43//! 3.  **Phase 3**: Run the feeder for 5 seconds to dispense more food.
44//! 4.  **Phase 4**: Wait another 15 seconds before all devices resume normal operation.
45//!
46//! This entire sequence would be contained within a single `Feedpattern` struct as a `Vec<FeedPhase>`.
47
48/// Holds the configuration data of one feed pattern.
49pub struct Feedpattern {
50    /// Numeric ID of the feed profile
51    /// Data is not used in the implementation. Therefore, it is tagged as unused.
52    #[allow(unused)]
53    pub profile_id: i32,
54
55    /// name of the feed profile
56    pub profile_name: String,
57
58    /// vector of type FeedPhase
59    pub feedphases: Vec<FeedPhase>,
60}
61
62impl Feedpattern {
63    /// Calculates the total duration (in seconds) that the feeder motor will be active for this feed pattern.
64    ///
65    /// This function iterates through all defined `FeedPhase`es within the pattern and sums
66    /// their individual `feed_duration` values. This total runtime is typically logged
67    /// as part of a completed feed event.
68    ///
69    /// # Returns
70    /// The total feeder run time for this pattern, expressed as an `f64` in seconds.
71    pub fn calc_feeder_runtime(&self) -> f64 {
72        self.feedphases
73            .iter()
74            .map(|phase| phase.feed_duration as f64)
75            .sum()
76    }
77}
78
79/// Holds the configuration data of one feed phase.
80#[derive(Clone, Debug, PartialEq)]
81pub struct FeedPhase {
82    /// duration of the pause before switching on the feeder motor in milliseconds
83    pub pause_duration: i16,
84
85    /// flag indicating if the protein skimmer shall remain switched on during the pause
86    pub pause_skimmer: bool,
87
88    /// flag indicating if the main pump #1 shall remain switched on during the pause
89    pub pause_main_pump_1: bool,
90
91    /// flag indicating if the main pump #2 shall remain switched on during the pause
92    pub pause_main_pump_2: bool,
93
94    /// flag indicating if the auxiliary pump #1 shall remain switched on during the pause
95    pub pause_aux_pump_1: bool,
96
97    /// flag indicating if the auxiliary pump #2 shall remain switched on during the pause
98    pub pause_aux_pump_2: bool,
99
100    /// duration of the feed after switching on the feeder motor in milliseconds
101    pub feed_duration: i16,
102
103    /// flag indicating if the protein skimmer shall remain switched on during the feed phase
104    pub feed_skimmer: bool,
105
106    /// flag indicating if the main pump #1 shall remain switched on during the feed phase
107    pub feed_main_pump_1: bool,
108
109    /// flag indicating if the main pump #2 shall remain switched on during the feed phase
110    pub feed_main_pump_2: bool,
111
112    /// flag indicating if the auxiliary pump #1 shall remain switched on during the feed phase
113    pub feed_aux_pump_1: bool,
114
115    /// flag indicating if the auxiliary pump #2 shall remain switched on during the feed phase
116    pub feed_aux_pump_2: bool,
117}
118
119impl FeedPhase {
120    #[cfg(test)]
121    // Creates a new `FeedPhase` struct with explicitly provided values.
122    //
123    // This constructor is intended exclusively for use in test environments.
124    // It allows for the direct creation of a `FeedPhase` instance by setting
125    // all its properties, bypassing typical deserialization or logic.
126    //
127    // # Arguments
128    // * `pause_duration` - The duration of the pause phase in milliseconds.
129    // * `pause_skimmer` - Flag indicating if the protein skimmer is active during the pause phase.
130    // * `pause_main_pump_1` - Flag indicating if main pump #1 is active during the pause phase.
131    // * `pause_main_pump_2` - Flag indicating if main pump #2 is active during the pause phase.
132    // * `pause_aux_pump_1` - Flag indicating if auxiliary pump #1 is active during the pause phase.
133    // * `pause_aux_pump_2` - Flag indicating if auxiliary pump #2 is active during the pause phase.
134    // * `feed_duration` - The duration of the feed phase in milliseconds.
135    // * `feed_skimmer` - Flag indicating if the protein skimmer is active during the feed phase.
136    // * `feed_main_pump_1` - Flag indicating if main pump #1 is active during the feed phase.
137    // * `feed_main_pump_2` - Flag indicating if main pump #2 is active during the feed phase.
138    // * `feed_aux_pump_1` - Flag indicating if auxiliary pump #1 is active during the feed phase.
139    // * `feed_aux_pump_2` - Flag indicating if auxiliary pump #2 is active during the feed phase.
140    //
141    // # Returns
142    // A new `FeedPhase` struct initialized with the provided values.
143    pub fn new(
144        pause_duration: i16,
145        pause_skimmer: bool,
146        pause_main_pump_1: bool,
147        pause_main_pump_2: bool,
148        pause_aux_pump_1: bool,
149        pause_aux_pump_2: bool,
150        feed_duration: i16,
151        feed_skimmer: bool,
152        feed_main_pump_1: bool,
153        feed_main_pump_2: bool,
154        feed_aux_pump_1: bool,
155        feed_aux_pump_2: bool,
156    ) -> FeedPhase {
157        FeedPhase {
158            pause_duration,
159            pause_skimmer,
160            pause_main_pump_1,
161            pause_main_pump_2,
162            pause_aux_pump_1,
163            pause_aux_pump_2,
164            feed_duration,
165            feed_skimmer,
166            feed_main_pump_1,
167            feed_main_pump_2,
168            feed_aux_pump_1,
169            feed_aux_pump_2,
170        }
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    /// Tests that `calc_feeder_runtime` returns 0.0 for a pattern with no feed phases.
179    #[test]
180    fn test_calc_feeder_runtime_empty() {
181        println!("* Testing calc_feeder_runtime with an empty phase list...");
182        let pattern = Feedpattern {
183            profile_id: 1,
184            profile_name: "Empty Pattern".to_string(),
185            feedphases: vec![],
186        };
187
188        assert_eq!(
189            pattern.calc_feeder_runtime(),
190            0.0,
191            "Runtime for an empty pattern should be 0.0"
192        );
193        println!("* Succeeded: Empty pattern runtime is correct.");
194    }
195
196    /// Tests that `calc_feeder_runtime` correctly calculates the runtime for a single feed phase.
197    #[test]
198    fn test_calc_feeder_runtime_single_phase() {
199        println!("* Testing calc_feeder_runtime with a single phase...");
200        let phase = FeedPhase::new(
201            1000, false, false, false, false, false, 2500, false, false, false, false, false,
202        );
203        let pattern = Feedpattern {
204            profile_id: 2,
205            profile_name: "Single Phase Pattern".to_string(),
206            feedphases: vec![phase],
207        };
208
209        assert_eq!(
210            pattern.calc_feeder_runtime(),
211            2500.0,
212            "Runtime for a single phase should match its feed_duration"
213        );
214        println!("* Succeeded: Single phase runtime is correct.");
215    }
216
217    /// Tests that `calc_feeder_runtime` correctly sums the durations from multiple feed phases.
218    #[test]
219    fn test_calc_feeder_runtime_multiple_phases() {
220        println!("* Testing calc_feeder_runtime with multiple phases...");
221        let phase1 = FeedPhase::new(
222            1000, false, false, false, false, false, 2000, false, false, false, false, false,
223        );
224        let phase2 = FeedPhase::new(
225            500, false, false, false, false, false, 3000, false, false, false, false, false,
226        );
227        let phase3 = FeedPhase::new(
228            0, false, false, false, false, false, 1500, false, false, false, false, false,
229        );
230
231        let pattern = Feedpattern {
232            profile_id: 3,
233            profile_name: "Multi-Phase Pattern".to_string(),
234            feedphases: vec![phase1, phase2, phase3],
235        };
236
237        // Expected: 2000 + 3000 + 1500 = 6500
238        assert_eq!(
239            pattern.calc_feeder_runtime(),
240            6500.0,
241            "Runtime for multiple phases should be the sum of their feed_durations"
242        );
243        println!("* Succeeded: Multi-phase runtime is correct.");
244    }
245
246    /// Tests that phases with a `feed_duration` of zero do not contribute to the total runtime.
247    #[test]
248    fn test_calc_feeder_runtime_with_zero_duration() {
249        println!("* Testing calc_feeder_runtime with zero-duration phases...");
250        let phase1 = FeedPhase::new(
251            1000, false, false, false, false, false, 5000, false, false, false, false, false,
252        );
253        // A phase that is only for pausing, with no feeding action.
254        let phase2 = FeedPhase::new(
255            10000, false, false, false, false, false, 0, false, false, false, false, false,
256        );
257        let phase3 = FeedPhase::new(
258            1000, false, false, false, false, false, 2500, false, false, false, false, false,
259        );
260
261        let pattern = Feedpattern {
262            profile_id: 4,
263            profile_name: "Zero Duration Pattern".to_string(),
264            feedphases: vec![phase1, phase2, phase3],
265        };
266
267        // Expected: 5000 + 0 + 2500 = 7500
268        assert_eq!(
269            pattern.calc_feeder_runtime(),
270            7500.0,
271            "Phases with zero feed_duration should not contribute to the total runtime"
272        );
273        println!("* Succeeded: Zero-duration phase is handled correctly.");
274    }
275
276    /// Tests the edge case where a `feed_duration` is negative, which is possible with the `i16` type.
277    #[test]
278    fn test_calc_feeder_runtime_with_negative_duration() {
279        println!("* Testing calc_feeder_runtime with a negative duration (edge case)...");
280        let phase1 = FeedPhase::new(
281            1000, false, false, false, false, false, 5000, false, false, false, false, false,
282        );
283        // A phase with a negative duration. This is invalid logically but possible with the i16 type.
284        let phase2 = FeedPhase::new(
285            1000, false, false, false, false, false, -1000, false, false, false, false, false,
286        );
287
288        let pattern = Feedpattern {
289            profile_id: 5,
290            profile_name: "Negative Duration Pattern".to_string(),
291            feedphases: vec![phase1, phase2],
292        };
293
294        // Expected: 5000 + (-1000) = 4000
295        assert_eq!(
296            pattern.calc_feeder_runtime(),
297            4000.0,
298            "Negative feed_duration should be summed correctly"
299        );
300        println!("* Succeeded: Negative duration edge case is handled correctly.");
301    }
302}