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}