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}