aquarium_control/relays/
actuate_controllino.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*/
9use crate::relays::actuate_controllino_config::ActuateControllinoConfig;
10use crate::relays::controllino_message::{ControllinoMessage, ControllinoMessageContent};
11use crate::relays::relay_error::RelayError;
12use crate::relays::relay_manager::RelayActuationTrait;
13use crate::utilities::channel_content::{AquariumDevice, InternalCommand};
14use log::{error, warn};
15use serialport::{ClearBuffer, SerialPort};
16use spin_sleep::SpinSleeper;
17use std::collections::HashSet;
18use std::time::Duration;
19use thiserror::Error;
20
21const CONTROLLINO_MAX_RELAY_ID: u8 = 64;
22const CONTROLLINO_MIN_RELAY_ID: u8 = 1;
23
24/// Contains constants for communication with Controllino
25#[allow(unused)]
26pub mod controllino_constants {
27    /// the size of ControllinoMessage is deliberately set here because checksums are added to the content of struct
28    pub const MESSAGE_SIZE: usize = 8;
29
30    /// command for setting a single relay
31    pub const COMMAND_SET_RELAY: char = 'S';
32
33    /// command for unsetting a single relay
34    pub const COMMAND_UNSET_RELAY: char = 'U';
35
36    /// command for pulsing a relay
37    pub const COMMAND_PULSE_RELAY: char = 'P';
38
39    /// command for getting relay status
40    pub const COMMAND_GET_RELAY_STATUS: char = 'G';
41
42    /// command for sending a heart beat from control application to Controllino
43    pub const COMMAND_HEARTBEAT: char = 'H';
44
45    /// command for setting a digital output    
46    pub const COMMAND_SET_DIGITAL_OUT: char = 'D';
47
48    /// command for unsetting a digital output    
49    pub const COMMAND_UNSET_DIGITAL_OUT: char = 'J';
50
51    /// command for getting digital IO status
52    pub const COMMAND_GET_DIGITAL_OUT_STATUS: char = 'G';
53
54    /// initial wait period after resetting DTR
55    pub const SLEEP_AFTER_DTR_RESET_MILLIS: u64 = 1000;
56
57    /// initial wait period after clearing buffer
58    pub const SLEEP_AFTER_CLEAR_BUFFER_MILLIS: u64 = 2000;
59}
60
61/// Calculates and validates the checksums of a byte sequence received from the Controllino.
62///
63/// This private helper function computes two XOR-based checksums from specific bytes
64/// within the input `data` array and compares them against the checksums embedded
65/// in the received data itself. It's crucial for verifying the integrity of
66/// messages sent by the Controllino device.
67///
68/// If the checksums do not match, a warning is logged with detailed information
69/// about the discrepancy, aiding in debugging communication issues.
70///
71/// # Arguments
72/// * `data` - A slice of `u8` bytes representing the message received from the Controllino.
73///   It is expected to be at least 8 bytes long.
74///
75/// # Returns
76/// - `true`: If both calculated checksums match the received checksums, indicating data integrity.
77/// - `false`: If the checksums do not match.
78fn check_controllino_checksums(data: &[u8]) -> bool {
79    let first_checksum = data[0] ^ data[1] ^ data[2];
80    let second_checksum = data[4] ^ data[5] ^ data[6];
81    let ok_result = (data[3] == first_checksum) && (data[7] == second_checksum);
82    if !ok_result {
83        warn!(target: module_path!(),
84            "data used for checksum calculation: \n\
85            [0]={} [1]={} [2]={} [3]={} [4]={} [5]={} [6]={} [7]={}\n\
86            first checksum ([3]) should have been {}\n\
87            second checksum ([7]) should have been {}",
88            data[0], data[1], data[2], data[3],
89            data[4], data[5], data[6], data[7],
90            first_checksum,
91            second_checksum,
92        );
93    }
94    ok_result
95}
96
97/// Contains error definitions for ActuateControllino
98#[derive(Error, Debug)]
99pub enum ActuateControllinoError {
100    /// Relay ID is outside the allowed range.
101    #[error("Relay ID for Controllino ({0}) is outside the allowed range ({1} ... {2}).")]
102    RelayIdOutsideRange(u8, u8, u8),
103
104    /// Found a duplicate value in definition of relay IDs.
105    #[error("Found a duplicate value ({0}) in definition of relay IDs.")]
106    RelayIdDuplicate(u8),
107
108    /// Port name for communication with Controllino is empty.
109    #[error("Port name for communication with Controllino is empty.")]
110    EmptyPortName,
111
112    /// Failed to open serial port for Controllino.
113    #[error("Failed to open serial port for Controllino.")]
114    SerialPortOpenFailed {
115        #[source]
116        source: serialport::Error,
117    },
118
119    /// Failed to set timeout for serial port communication.
120    #[error(
121        "Could not set timeout of {timeout_millis} milliseconds for serial port communication."
122    )]
123    SetTimeoutFailed {
124        timeout_millis: u64,
125
126        #[source]
127        source: serialport::Error,
128    },
129
130    /// Failed to clear DTR of serial port communication.
131    #[error("Failed to clear DTR of serial port communication.")]
132    ClearDTRFailed {
133        #[source]
134        source: serialport::Error,
135    },
136
137    /// Failed to clear buffer of serial port communication.
138    #[error("Failed to clear buffer of serial port communication.")]
139    ClearBufferFailed {
140        #[source]
141        source: serialport::Error,
142    },
143}
144
145/// Contains relay configuration and trait implementation for actuation using Controllino hardware
146pub struct ActuateControllino {
147    /// Configuration for setup of Controllino device
148    config: ActuateControllinoConfig,
149
150    /// Object is used frequently to wait for a determined period of time, so storing it instead of recreating when needed.
151    /// Attribute is public because access from within test cases.
152    pub spin_sleeper: SpinSleeper,
153
154    /// Object is used frequently to wait for a determined period of time, so storing it instead of recreating when needed.
155    /// Attribute is public because access from within test cases.
156    pub controllino_processing_duration: Duration,
157
158    /// Interface to serial port for communication via USB
159    /// Attribute is public because access from within test cases.
160    pub serial_port_opt: Option<Box<dyn SerialPort>>,
161}
162
163/// Creates the message to be sent to Controllino based on an internal command.
164///
165/// This function translates a high-level `InternalCommand` into a low-level,
166/// hardware-specific `ControllinoMessage` ready for serial transmission.
167///
168/// # Arguments
169/// * `internal_command` - The `InternalCommand` specifying the device and action.
170/// * `config` - A reference to the `ActuateControllinoConfig` for device-to-relay mappings.
171///
172/// # Returns
173/// A `Result` containing the constructed `ControllinoMessage` on success.
174///
175/// # Errors
176/// Returns a `RelayError` if the `internal_command` is not applicable for actuation
177/// (e.g., `ResetAllErrors`) or if `get_relay_command` fails for other reasons.
178fn create_command_message(
179    internal_command: InternalCommand,
180    config: &ActuateControllinoConfig,
181) -> Result<ControllinoMessage, RelayError> {
182    match internal_command {
183        InternalCommand::SwitchOn(ref c)
184        | InternalCommand::SwitchOff(ref c)
185        | InternalCommand::Pulse(ref c, _) => {
186            let relay_command = get_relay_command(&internal_command, c.clone(), config)?;
187
188            let (relay_command_char, relay_id, duration_millis) = match relay_command {
189                InternalCommand::SetRelay(relay_id) => {
190                    // relay id is set inside this crate, so the cast is safe
191                    (controllino_constants::COMMAND_SET_RELAY, relay_id as u8, 0)
192                }
193                InternalCommand::UnsetRelay(relay_id) => {
194                    // relay id is set inside this crate, so the cast is safe
195                    (
196                        controllino_constants::COMMAND_UNSET_RELAY,
197                        relay_id as u8,
198                        0,
199                    )
200                }
201                InternalCommand::PulseRelay(relay_id, duration_millis) => {
202                    // relay id is set inside this crate, so the cast is safe
203                    (
204                        controllino_constants::COMMAND_PULSE_RELAY,
205                        relay_id as u8,
206                        duration_millis,
207                    )
208                }
209                _ => {
210                    return Err(RelayError::IrrelevantCommand(
211                        module_path!().to_string(),
212                        internal_command,
213                    ));
214                }
215            };
216            let controllino_message_content =
217                ControllinoMessageContent::new(relay_command_char, relay_id, duration_millis);
218            let controllino_message = ControllinoMessage::new(controllino_message_content);
219            Ok(controllino_message)
220        }
221        _ => Err(RelayError::IrrelevantCommand(
222            module_path!().to_string(),
223            internal_command,
224        )),
225    }
226}
227
228/// Creates a heartbeat message to be sent to the Controllino.
229///
230/// This function constructs a standard `ControllinoMessage` with the heartbeat
231/// command character. This is used to periodically signal to the hardware
232/// that the control software is still active.
233///
234/// # Returns
235/// A new `ControllinoMessage` configured for a heartbeat.
236fn create_heartbeat_message() -> ControllinoMessage {
237    let message_content =
238        ControllinoMessageContent::new(controllino_constants::COMMAND_HEARTBEAT, 0, 0);
239    ControllinoMessage::new(message_content)
240}
241
242/// Creates a Controllino-specific message from a high-level internal command.
243///
244/// This private helper function translates a generic `InternalCommand` (like `SwitchOn`, `SwitchOff`, or `Pulse`)
245/// into the precise `ControllinoMessage` format required for serial communication with the Controllino hardware.
246/// It uses the provided configuration to map `AquariumDevice` types to their
247/// corresponding relay IDs and to determine the correct relay actuation logic (e.g., `SetRelay` vs. `UnsetRelay`).
248///
249/// # Arguments
250/// * `internal_command` - The `InternalCommand` enum value representing the desired action (e.g., switching a device on/off or pulsing a relay).
251/// * `config` - A reference to the `ActuateControllinoConfig`, which provides device-to-relay ID mappings and other settings.
252///
253/// # Returns
254/// A `Result` containing the low-level `InternalCommand` (e.g., `SetRelay`, `PulseRelay`) on success.
255///
256/// # Errors
257/// This function will return a `RelayError` if:
258/// - The `internal_command` is not one that this function can translate for Controllino actuation (e.g., `ResetAllErrors`), resulting in `IrrelevantCommand`.
259/// - A `Pulse` command is issued for a device that does not support pulsing, resulting in `PulseNotAllowedForDevice`.
260pub fn get_relay_command(
261    internal_command: &InternalCommand,
262    device: AquariumDevice,
263    config: &ActuateControllinoConfig,
264) -> Result<InternalCommand, RelayError> {
265    let (relay_id, is_inverted, supports_pulse) = match device {
266        // Inverted logic, no pulse
267        AquariumDevice::Skimmer => (config.skimmer_id, true, false),
268        AquariumDevice::MainPump1 => (config.main_pump1_id, true, false),
269        AquariumDevice::MainPump2 => (config.main_pump2_id, true, false),
270        AquariumDevice::AuxPump1 => (config.aux_pump1_id, true, false),
271        AquariumDevice::AuxPump2 => (config.aux_pump2_id, true, false),
272
273        // Normal logic, no pulse
274        AquariumDevice::Heater => (config.heater_id, false, false),
275        AquariumDevice::Ventilation => (config.ventilation_id, false, false),
276        AquariumDevice::RefillPump => (config.refill_pump_id, false, false),
277        AquariumDevice::Feeder => (config.feeder_id, false, false),
278
279        // Normal logic, with pulse
280        AquariumDevice::PeristalticPump1 => (config.peristaltic_pump1_id, false, true),
281        AquariumDevice::PeristalticPump2 => (config.peristaltic_pump2_id, false, true),
282        AquariumDevice::PeristalticPump3 => (config.peristaltic_pump3_id, false, true),
283        AquariumDevice::PeristalticPump4 => (config.peristaltic_pump4_id, false, true),
284    };
285
286    match internal_command {
287        InternalCommand::SwitchOn(_) => {
288            if is_inverted {
289                Ok(InternalCommand::UnsetRelay(relay_id as u16))
290            } else {
291                Ok(InternalCommand::SetRelay(relay_id as u16))
292            }
293        }
294        InternalCommand::SwitchOff(_) => {
295            if is_inverted {
296                Ok(InternalCommand::SetRelay(relay_id as u16))
297            } else {
298                Ok(InternalCommand::UnsetRelay(relay_id as u16))
299            }
300        }
301        InternalCommand::Pulse(_, t) => {
302            if supports_pulse {
303                Ok(InternalCommand::PulseRelay(relay_id as u16, *t))
304            } else {
305                Err(RelayError::PulseNotAllowedForDevice(
306                    module_path!().to_string(),
307                    device,
308                ))
309            }
310        }
311        _ => Err(RelayError::IrrelevantCommand(
312            module_path!().to_string(),
313            internal_command.clone(),
314        )),
315    }
316}
317
318impl RelayActuationTrait for ActuateControllino {
319    /// Actuates a specific relay on the Controllino hardware via serial port communication.
320    ///
321    /// This function translates high-level `InternalCommand`s (`SwitchOn`, `SwitchOff`, `Pulse`)
322    /// into low-level serial messages for the Controllino. It sends the command, waits for
323    /// the Controllino to process it (including any pulse durations), and then reads
324    /// and validates the response, including checksum verification.
325    ///
326    /// # Arguments
327    /// * `internal_command` - The `InternalCommand` specifying the device and desired action (e.g., `SwitchOn(Heater)`, `Pulse(PeristalticPump1, 500)`).
328    ///
329    /// # Returns
330    /// An empty `Result` (`Ok(())`) if the command was sent successfully and a valid acknowledgment was received from the Controllino.
331    ///
332    /// # Errors
333    /// This function will return a `RelayError` if any step in the communication process fails:
334    /// - `FailureCommandMessageCreation`: If there's an issue preparing the Controllino message.
335    /// - `WriteError`: If the command cannot be written to the serial port.
336    /// - `ReadError`: If a response cannot be read from the serial port within the timeout.
337    /// - `IncorrectChecksum`: If the received response has an invalid checksum, indicating data corruption.
338    /// - `SerialPortNotConfigured`: If the serial port was not initialized (e.g., `active` is false in config).
339    /// - `IrrelevantCommand`: If an inapplicable command (e.g., `ResetAllErrors`) is provided.
340    fn actuate(&mut self, internal_command: &InternalCommand) -> Result<(), RelayError> {
341        match internal_command {
342            InternalCommand::SwitchOn(ref _c)
343            | InternalCommand::SwitchOff(ref _c)
344            | InternalCommand::Pulse(ref _c, _) => {
345                // create a message to be sent to Controllino
346                let message = match create_command_message(internal_command.clone(), &self.config) {
347                    Ok(c) => c,
348                    Err(e) => {
349                        error!(
350                            target: module_path!(),
351                            "preparation of communication failed ({e:?})"
352                        );
353                        return Err(RelayError::FailureCommandMessageCreation(
354                            module_path!().to_string(),
355                        ));
356                    }
357                };
358
359                // write a message to Controllino
360                if let Some(serial_port) = self.serial_port_opt.as_mut() {
361                    match serial_port.write(&message.data) {
362                        Ok(amount_byte_written) => {
363                            if amount_byte_written != message.data.len() {
364                                warn!(
365                                        "{}: Writing to Controllino device failed. Amount of bytes written: {}. Expected: {}",
366                                        module_path!(),
367                                        amount_byte_written,
368                                        message.data.len()
369                                    );
370                            }
371                        }
372                        Err(e) => {
373                            error!(
374                                target: module_path!(),
375                                "sending command to Controllino failed ({e:?})"
376                            );
377                            return Err(RelayError::WriteError(module_path!().to_string()));
378                        }
379                    }
380
381                    // give Controllino some time to process the command
382                    self.spin_sleeper
383                        .sleep(self.controllino_processing_duration);
384
385                    // for pulse command, also wait the pulse duration
386                    match internal_command {
387                        InternalCommand::Pulse(_, t) => {
388                            let pulse_duration_millis = *t;
389                            let pulse_duration =
390                                Duration::from_millis(pulse_duration_millis as u64);
391                            self.spin_sleeper.sleep(pulse_duration);
392                        }
393                        _ => { /* do nothing */ }
394                    }
395
396                    // read response from Controllino
397                    let mut serial_buf: Vec<u8> = vec![0; controllino_constants::MESSAGE_SIZE];
398                    match serial_port.read_exact(serial_buf.as_mut_slice()) {
399                        Ok(()) => {
400                            if !check_controllino_checksums(serial_buf.as_slice()) {
401                                warn!(
402                                    target: module_path!(),
403                                    "Received response from device with incorrect checksums"
404                                );
405                                Err(RelayError::IncorrectChecksum(module_path!().to_string()))
406                            } else {
407                                Ok(())
408                            }
409                        }
410                        Err(_) => {
411                            error!(
412                                target: module_path!(),
413                                "receiving response from device failed."
414                            );
415                            Err(RelayError::ReadError(module_path!().to_string()))
416                        }
417                    }
418                } else {
419                    Err(RelayError::SerialPortNotConfigured(
420                        module_path!().to_string(),
421                    ))
422                }
423            }
424            _ => {
425                warn!(
426                    target: module_path!(),
427                    "ignoring irrelevant internal command {internal_command}."
428                );
429                Err(RelayError::IrrelevantCommand(
430                    module_path!().to_string(),
431                    internal_command.clone(),
432                ))
433            }
434        }
435    }
436
437    /// Gets the configured heartbeat interval in seconds.
438    ///
439    /// This function returns the interval at which the `heartbeat` method should be called
440    /// to keep the hardware connection alive.
441    ///
442    /// # Returns
443    /// An `Option<u64>` containing the heartbeat interval in seconds. It returns `Some`
444    /// for this implementation as the interval is always configured.
445    fn get_heartbeat_interval_seconds(&self) -> Option<u64> {
446        Some(self.config.heartbeat_interval_seconds)
447    }
448
449    /// Sends a heartbeat signal to the Controllino to indicate the software is active.
450    ///
451    /// # Returns
452    /// An empty `Result` (`Ok(())`) on success.
453    ///
454    /// # Errors
455    /// This function will return a `RelayError` if:
456    /// - The serial port is not configured (`SerialPortNotConfigured`).
457    /// - Sending the heartbeat message over the serial port fails (`SendToSerialPortFailed`).
458    fn heartbeat(&mut self) -> Result<(), RelayError> {
459        if let Some(serial_port) = self.serial_port_opt.as_mut() {
460            // create a message to be sent to Controllino
461            let message = create_heartbeat_message();
462
463            self.spin_sleeper
464                .sleep(self.controllino_processing_duration);
465
466            // write a message to Controllino
467            match serial_port.write(&message.data) {
468                Ok(amount_byte_written) => {
469                    if amount_byte_written != message.data.len() {
470                        warn!(
471                            "{}: Writing heartbeat message to Controllino device failed. Amount of bytes written: {}. Expected: {}",
472                            module_path!(),
473                            amount_byte_written,
474                            message.data.len()
475                        );
476                    }
477                }
478                Err(e) => {
479                    return Err(RelayError::SendToSerialPortFailed {
480                        location: module_path!().to_string(),
481                        source: e,
482                    });
483                }
484            }
485
486            self.spin_sleeper
487                .sleep(self.controllino_processing_duration);
488        }
489        Ok(())
490    }
491
492    /// Flushes the serial communication buffer to recover from framing or synchronization errors.
493    ///
494    /// The root cause is usually a desynchronization between the sender (Controllino) and receiver (Raspberry Pi). This can happen due to:
495    /// - Noise on the line: A stray bit flip could make the Pi misinterpret the start of a message.
496    /// - Timing issues: The Pi's read might start too early or too late relative to when the Controllino actually begins sending data.
497    /// - Buffer state: Leftover bytes from a previous, incomplete, or corrupted transmission can throw off further reads.
498    ///
499    /// # Returns
500    /// An empty `Result` (`Ok(())`) on success.
501    ///
502    /// # Errors
503    /// This function will return a `RelayError` if:
504    /// - The serial port is not configured (`SerialPortNotConfigured`).
505    /// - Clearing the serial port buffer fails (`SerialPortFlushFailed`).
506    fn flush_buffer(&mut self) -> Result<(), RelayError> {
507        if let Some(serial_port) = self.serial_port_opt.as_mut() {
508            match serial_port.clear(ClearBuffer::All) {
509                Ok(_) => {
510                    warn!(
511                        "{}: flushed Controllino communication buffer.",
512                        module_path!()
513                    );
514                }
515                Err(e) => {
516                    return Err(RelayError::SerialPortFlushFailed {
517                        location: module_path!().to_string(),
518                        source: e,
519                    });
520                }
521            }
522
523            self.spin_sleeper
524                .sleep(self.controllino_processing_duration);
525        }
526        Ok(())
527    }
528}
529
530/// Opens and initializes a serial port connection to the Controllino device.
531///
532/// This function attempts to establish a serial communication link using the
533/// provided port name and baud rate. It also configures the port timeout,
534/// performs a DTR reset, and clears input/output buffers to ensure a clean
535/// communication state before returning the opened port.
536///
537/// # Arguments
538/// * `port_name` - The name of the serial port (e.g., "/dev/ttyUSB0" on Linux).
539/// * `baud_rate` - The baud rate for serial communication (e.g., 9600).
540/// * `timeout_millis` - The read/write timeout for serial port operations in milliseconds.
541///
542/// # Returns
543/// A `Result` containing a `Box<dyn SerialPort>` representing the opened and configured serial port on success.
544///
545/// # Errors
546/// Returns an `ActuateControllinoError` if any step of the initialization fails:
547/// - `SerialPortOpenFailed`: If the port cannot be opened.
548/// - `SetTimeoutFailed`: If the timeout cannot be set on the port.
549/// - `ClearDTRFailed`: If the Data Terminal Ready signal cannot be cleared.
550/// - `ClearBufferFailed`: If the serial port's internal buffers cannot be cleared.
551fn controllino_open_serial_port(
552    port_name: String,
553    baud_rate: u32,
554    timeout_millis: u64,
555) -> Result<Box<dyn SerialPort>, ActuateControllinoError> {
556    let spin_sleeper = SpinSleeper::default();
557
558    let mut serial_port = serialport::new(port_name.clone(), baud_rate)
559        .open()
560        .map_err(|e| ActuateControllinoError::SerialPortOpenFailed { source: e })?;
561
562    let timeout_duration = Duration::from_millis(timeout_millis);
563    serial_port.set_timeout(timeout_duration).map_err(|e| {
564        ActuateControllinoError::SetTimeoutFailed {
565            timeout_millis,
566            source: e,
567        }
568    })?;
569
570    // write to Data Terminal Ready pin: clear the signal
571    serial_port
572        .write_data_terminal_ready(false)
573        .map_err(|e| ActuateControllinoError::ClearDTRFailed { source: e })?;
574
575    // wait for the device
576    let sleep_duration_after_dtr_reset =
577        Duration::from_millis(controllino_constants::SLEEP_AFTER_DTR_RESET_MILLIS);
578    spin_sleeper.sleep(sleep_duration_after_dtr_reset);
579
580    // flush the buffer
581    serial_port
582        .clear(ClearBuffer::All)
583        .map_err(|e| ActuateControllinoError::ClearBufferFailed { source: e })?;
584
585    // wait for the device
586    let sleep_duration_after_buffer_clear =
587        Duration::from_millis(controllino_constants::SLEEP_AFTER_CLEAR_BUFFER_MILLIS);
588    spin_sleeper.sleep(sleep_duration_after_buffer_clear);
589
590    Ok(serial_port)
591}
592
593impl ActuateControllino {
594    /// Validates the provided relay ID configuration for the Controllino.
595    ///
596    /// This function performs two critical checks:
597    /// 1. Ensures that all configured relay IDs fall within the permissible range
598    ///    defined by `CONTROLLINO_MIN_RELAY_ID` and `CONTROLLINO_MAX_RELAY_ID`.
599    /// 2. It confirms that all assigned relay IDs are unique, preventing conflicts
600    ///    where multiple devices might be configured to the same physical relay.
601    ///
602    /// # Arguments
603    /// * `config` - A reference to the `ActuateControllinoConfig` containing the
604    ///   relay ID mappings for various aquarium devices.
605    ///
606    /// # Returns
607    /// An empty `Result` (`Ok(())`) if the configuration is valid.
608    ///
609    /// # Errors
610    /// Returns an `ActuateControllinoError` if an invalid configuration is found:
611    /// - `RelayIdOutsideRange`: If a relay ID is less than the minimum or greater than the maximum allowed value.
612    /// - `RelayIdDuplicate`: If the same relay ID is assigned to more than one device.
613    pub fn check_valid_relay_id_config(
614        config: &ActuateControllinoConfig,
615    ) -> Result<(), ActuateControllinoError> {
616        // check if relay ids are in valid range and if they are distinct
617        let mut seen_values = HashSet::new();
618
619        let relay_id_values = [
620            config.aux_pump1_id,
621            config.aux_pump2_id,
622            config.feeder_id,
623            config.heater_id,
624            config.main_pump1_id,
625            config.main_pump2_id,
626            config.peristaltic_pump1_id,
627            config.peristaltic_pump2_id,
628            config.peristaltic_pump3_id,
629            config.peristaltic_pump4_id,
630            config.refill_pump_id,
631            config.ventilation_id,
632        ];
633
634        for &value in &relay_id_values {
635            if !(CONTROLLINO_MIN_RELAY_ID..=CONTROLLINO_MAX_RELAY_ID).contains(&value) {
636                return Err(ActuateControllinoError::RelayIdOutsideRange(
637                    value,
638                    CONTROLLINO_MIN_RELAY_ID,
639                    CONTROLLINO_MAX_RELAY_ID,
640                ));
641            }
642            if !seen_values.insert(value) {
643                return Err(ActuateControllinoError::RelayIdDuplicate(value));
644            }
645        }
646
647        Ok(())
648    }
649
650    /// Creates a new `ActuateControllino` instance, establishing communication with the Controllino hardware.
651    ///
652    /// This constructor performs essential setup for hardware actuation. It validates the provided
653    /// relay ID configuration for correctness and uniqueness, ensures a serial port name is provided,
654    /// and then opens and initializes the serial port connection to the Controllino device if `active` is true.
655    ///
656    /// # Arguments
657    /// * `config` - Configuration data for the Controllino relays, which is moved into the new instance.
658    ///
659    /// # Returns
660    /// A `Result` containing a new `ActuateControllino` struct, ready for sending commands to the hardware.
661    ///
662    /// # Errors
663    /// Returns an `ActuateControllinoError` if any part of the setup fails:
664    /// - If `check_valid_relay_id_config` finds an error in the relay IDs.
665    /// - `EmptyPortName`: If the `port_name` in the configuration is an empty string.
666    /// - If `controllino_open_serial_port` fails to open and configure the serial port.
667    pub fn new(
668        config: ActuateControllinoConfig,
669    ) -> Result<ActuateControllino, ActuateControllinoError> {
670        let spin_sleeper = SpinSleeper::default();
671
672        // check the validity of configuration
673        Self::check_valid_relay_id_config(&config)?;
674
675        if config.port_name.is_empty() {
676            return Err(ActuateControllinoError::EmptyPortName);
677        }
678
679        let mut serial_port_opt: Option<Box<dyn SerialPort>> = None;
680        if config.active {
681            serial_port_opt = Some(controllino_open_serial_port(
682                config.port_name.clone(),
683                config.baud_rate,
684                config.timeout_millis,
685            )?);
686        }
687        let controllino_processing_millis = config.controllino_processing_millis;
688        let controllino_processing_duration = Duration::from_millis(controllino_processing_millis);
689
690        Ok(ActuateControllino {
691            config,
692            spin_sleeper,
693            controllino_processing_duration,
694            serial_port_opt,
695        })
696    }
697}
698
699#[cfg(test)]
700pub mod tests {
701    // feature-specific imports
702    cfg_if::cfg_if! {
703        if #[cfg(feature = "controllino_hw")] {
704            use serialport::SerialPort;
705            use spin_sleep::SpinSleeper;
706            use std::time::Duration;
707
708            use crate::relays::controllino::{Controllino, RelayActuationTrait};
709            use crate::relays::controllino_actuate_hardware::ControllinoActuateHardware;
710        }
711    }
712
713    use crate::utilities::channel_content::AquariumDevice::{
714        AuxPump1, AuxPump2, Feeder, Heater, MainPump1, MainPump2, PeristalticPump1,
715        PeristalticPump2, PeristalticPump3, PeristalticPump4, Skimmer, Ventilation,
716    };
717
718    use crate::relays::actuate_controllino::{
719        controllino_constants, create_command_message, ActuateControllino, ActuateControllinoError,
720        CONTROLLINO_MAX_RELAY_ID, CONTROLLINO_MIN_RELAY_ID,
721    };
722    use crate::relays::relay_error::RelayError;
723    use crate::utilities::channel_content::InternalCommand;
724    use crate::utilities::config::{read_config_file, ConfigData};
725
726    #[test]
727    pub fn test_invalid_relay_id_config_low() {
728        let mut config: ConfigData =
729            read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
730
731        config.controllino_relay.main_pump1_id = CONTROLLINO_MIN_RELAY_ID - 1;
732
733        let test_result = ActuateControllino::new(config.controllino_relay);
734        assert!(matches!(
735            test_result,
736            Err(ActuateControllinoError::RelayIdOutsideRange(_, _, _))
737        ));
738    }
739    #[test]
740    pub fn test_invalid_relay_id_config_high() {
741        let mut config: ConfigData =
742            read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
743
744        config.controllino_relay.main_pump1_id = CONTROLLINO_MAX_RELAY_ID + 1;
745
746        let test_result = ActuateControllino::new(config.controllino_relay);
747        assert!(matches!(
748            test_result,
749            Err(ActuateControllinoError::RelayIdOutsideRange(_, _, _))
750        ));
751    }
752
753    #[test]
754    pub fn test_invalid_relay_config_duplicate() {
755        let mut config: ConfigData =
756            read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
757
758        config.controllino_relay.main_pump1_id = CONTROLLINO_MAX_RELAY_ID;
759        config.controllino_relay.main_pump2_id = CONTROLLINO_MAX_RELAY_ID;
760
761        let test_result = ActuateControllino::new(config.controllino_relay);
762        assert!(matches!(
763            test_result,
764            Err(ActuateControllinoError::RelayIdDuplicate(_))
765        ));
766    }
767
768    #[cfg(all(target_os = "macos", feature = "controllino_hw"))]
769    fn get_platform_specific_configuration_file() -> String {
770        "/config/aquarium_control_test_controllino_macos.toml".to_string()
771    }
772
773    #[cfg(all(target_os = "linux", feature = "controllino_hw"))]
774    fn get_platform_specific_configuration_file() -> String {
775        "/config/aquarium_control_test_controllino_linux.toml".to_string()
776    }
777
778    #[cfg(feature = "controllino_hw")]
779    #[test]
780    // Test case actuates on real hardware.
781    // Execution of this test case requires serial port connection to the Controllino device.
782    pub fn test_controllino_switch_on_off_pulse() {
783        let sleep_duration_500_millis = Duration::from_millis(500);
784        let spin_sleeper = SpinSleeper::default();
785
786        let config: ConfigData = read_config_file(get_platform_specific_configuration_file());
787        let controllino = Controllino::new(config.relay_manager);
788
789        let config2: ConfigData = read_config_file(get_platform_specific_configuration_file());
790
791        let serial_port: Box<dyn SerialPort>;
792        serial_port = match controllino.serial_port_opt.as_ref().unwrap().try_clone() {
793            Ok(c) => c,
794            Err(_) => {
795                panic!("Controllino: Error occurred when trying to clone serial port.");
796            }
797        };
798
799        let mut actuator = ControllinoActuateHardware;
800
801        let expect_info =
802            "Controllino test_switch_on_off_pulse failed cloning serial port interface.";
803
804        // Request Controllino to switch on and off all relays sequentially
805        assert_eq!(
806            actuator.actuate(InternalCommand::SwitchOn(Skimmer),),
807            Ok(())
808        );
809        spin_sleeper.sleep(sleep_duration_500_millis);
810        assert_eq!(
811            actuator.actuate(InternalCommand::SwitchOn(Ventilation),),
812            Ok(())
813        );
814        spin_sleeper.sleep(sleep_duration_500_millis);
815        assert_eq!(actuator.actuate(InternalCommand::SwitchOn(Heater),), Ok(()));
816        spin_sleeper.sleep(sleep_duration_500_millis);
817        assert_eq!(
818            actuator.actuate(InternalCommand::SwitchOn(MainPump1),),
819            Ok(())
820        );
821        spin_sleeper.sleep(sleep_duration_500_millis);
822        assert_eq!(
823            actuator.actuate(InternalCommand::SwitchOn(MainPump2),),
824            Ok(())
825        );
826        spin_sleeper.sleep(sleep_duration_500_millis);
827        assert_eq!(
828            actuator.actuate(InternalCommand::SwitchOn(AuxPump1),),
829            Ok(())
830        );
831        spin_sleeper.sleep(sleep_duration_500_millis);
832        assert_eq!(
833            actuator.actuate(InternalCommand::SwitchOn(AuxPump2),),
834            Ok(())
835        );
836        spin_sleeper.sleep(sleep_duration_500_millis);
837        assert_eq!(
838            actuator.actuate(InternalCommand::SwitchOn(PeristalticPump1),),
839            Ok(())
840        );
841        spin_sleeper.sleep(sleep_duration_500_millis);
842        assert_eq!(
843            actuator.actuate(InternalCommand::SwitchOn(PeristalticPump2),),
844            Ok(())
845        );
846        spin_sleeper.sleep(sleep_duration_500_millis);
847        assert_eq!(
848            actuator.actuate(InternalCommand::SwitchOn(PeristalticPump3),),
849            Ok(())
850        );
851        spin_sleeper.sleep(sleep_duration_500_millis);
852        assert_eq!(
853            actuator.actuate(InternalCommand::SwitchOn(PeristalticPump4),),
854            Ok(())
855        );
856        spin_sleeper.sleep(sleep_duration_500_millis);
857        assert_eq!(actuator.actuate(InternalCommand::SwitchOn(Feeder),), Ok(()));
858        spin_sleeper.sleep(sleep_duration_500_millis);
859        assert_eq!(
860            actuator.actuate(InternalCommand::SwitchOff(Skimmer),),
861            Ok(())
862        );
863        spin_sleeper.sleep(sleep_duration_500_millis);
864        assert_eq!(
865            actuator.actuate(InternalCommand::SwitchOff(Ventilation),),
866            Ok(())
867        );
868        spin_sleeper.sleep(sleep_duration_500_millis);
869        assert_eq!(
870            actuator.actuate(InternalCommand::SwitchOff(Heater),),
871            Ok(())
872        );
873        spin_sleeper.sleep(sleep_duration_500_millis);
874        assert_eq!(
875            actuator.actuate(InternalCommand::SwitchOff(MainPump1),),
876            Ok(())
877        );
878        spin_sleeper.sleep(sleep_duration_500_millis);
879        assert_eq!(
880            actuator.actuate(InternalCommand::SwitchOff(MainPump2),),
881            Ok(())
882        );
883        spin_sleeper.sleep(sleep_duration_500_millis);
884        assert_eq!(
885            actuator.actuate(InternalCommand::SwitchOff(AuxPump1),),
886            Ok(())
887        );
888        spin_sleeper.sleep(sleep_duration_500_millis);
889        assert_eq!(
890            actuator.actuate(InternalCommand::SwitchOff(AuxPump2),),
891            Ok(())
892        );
893        spin_sleeper.sleep(sleep_duration_500_millis);
894        assert_eq!(
895            actuator.actuate(InternalCommand::SwitchOff(PeristalticPump1),),
896            Ok(())
897        );
898        spin_sleeper.sleep(sleep_duration_500_millis);
899        assert_eq!(
900            actuator.actuate(InternalCommand::SwitchOff(PeristalticPump2),),
901            Ok(())
902        );
903        spin_sleeper.sleep(sleep_duration_500_millis);
904        assert_eq!(
905            actuator.actuate(InternalCommand::SwitchOff(PeristalticPump3),),
906            Ok(())
907        );
908        spin_sleeper.sleep(sleep_duration_500_millis);
909        assert_eq!(
910            actuator.actuate(InternalCommand::SwitchOff(PeristalticPump4),),
911            Ok(())
912        );
913        spin_sleeper.sleep(sleep_duration_500_millis);
914        assert_eq!(
915            actuator.actuate(InternalCommand::SwitchOff(Feeder),),
916            Ok(())
917        );
918        spin_sleeper.sleep(sleep_duration_500_millis);
919
920        // Request Controllino to pulse all relays for peristaltic pumps
921        assert_eq!(
922            actuator.actuate(InternalCommand::Pulse(PeristalticPump1, 500),),
923            Ok(())
924        );
925        spin_sleeper.sleep(sleep_duration_500_millis);
926        assert_eq!(
927            actuator.actuate(InternalCommand::Pulse(PeristalticPump2, 500),),
928            Ok(())
929        );
930        spin_sleeper.sleep(sleep_duration_500_millis);
931        assert_eq!(
932            actuator.actuate(InternalCommand::Pulse(PeristalticPump3, 500),),
933            Ok(())
934        );
935        spin_sleeper.sleep(sleep_duration_500_millis);
936        assert_eq!(
937            actuator.actuate(InternalCommand::Pulse(PeristalticPump4, 500),),
938            Ok(())
939        );
940    }
941
942    #[test]
943    /// Test case tests if an invalid command is rejected when preparing the message for Controllino.
944    pub fn test_create_message_invalid_command() {
945        let config: ConfigData =
946            read_config_file("/config/aquarium_control_test_simulator.toml".to_string()).unwrap();
947        let test_result =
948            create_command_message(InternalCommand::ResetAllErrors, &config.controllino_relay);
949
950        assert!(matches!(
951            test_result,
952            Err(RelayError::IrrelevantCommand(
953                _,
954                InternalCommand::ResetAllErrors
955            )),
956        ));
957    }
958
959    #[test]
960    // Test case tests the implementation of the relay logic.
961    // During the creation of the message to Controllino, the application considers
962    // if relay needs to bet set or unset to switch on/off a device.
963    pub fn test_controllino_create_message_switch_on_with_set_relay_switch_off_with_unset_relay() {
964        let config: ConfigData =
965            read_config_file("/config/aquarium_control_test_simulator.toml".to_string()).unwrap();
966
967        // switching on all devices
968        let mut message = create_command_message(
969            InternalCommand::SwitchOn(Skimmer),
970            &config.controllino_relay,
971        )
972        .unwrap();
973        assert_eq!(
974            message.data[0],
975            controllino_constants::COMMAND_UNSET_RELAY as u8
976        );
977        assert_eq!(message.data[1], config.controllino_relay.skimmer_id);
978
979        message = create_command_message(
980            InternalCommand::SwitchOn(Ventilation),
981            &config.controllino_relay,
982        )
983        .unwrap();
984        assert_eq!(
985            message.data[0],
986            controllino_constants::COMMAND_SET_RELAY as u8
987        );
988        assert_eq!(message.data[1], config.controllino_relay.ventilation_id);
989
990        message =
991            create_command_message(InternalCommand::SwitchOn(Heater), &config.controllino_relay)
992                .unwrap();
993        assert_eq!(
994            message.data[0],
995            controllino_constants::COMMAND_SET_RELAY as u8
996        );
997        assert_eq!(message.data[1], config.controllino_relay.heater_id);
998
999        message = create_command_message(
1000            InternalCommand::SwitchOn(MainPump1),
1001            &config.controllino_relay,
1002        )
1003        .unwrap();
1004        assert_eq!(
1005            message.data[0],
1006            controllino_constants::COMMAND_UNSET_RELAY as u8
1007        );
1008        assert_eq!(message.data[1], config.controllino_relay.main_pump1_id);
1009
1010        message = create_command_message(
1011            InternalCommand::SwitchOn(MainPump2),
1012            &config.controllino_relay,
1013        )
1014        .unwrap();
1015        assert_eq!(
1016            message.data[0],
1017            controllino_constants::COMMAND_UNSET_RELAY as u8
1018        );
1019        assert_eq!(message.data[1], config.controllino_relay.main_pump2_id);
1020
1021        message = create_command_message(
1022            InternalCommand::SwitchOn(AuxPump1),
1023            &config.controllino_relay,
1024        )
1025        .unwrap();
1026        assert_eq!(
1027            message.data[0],
1028            controllino_constants::COMMAND_UNSET_RELAY as u8
1029        );
1030        assert_eq!(message.data[1], config.controllino_relay.aux_pump1_id);
1031
1032        message = create_command_message(
1033            InternalCommand::SwitchOn(AuxPump2),
1034            &config.controllino_relay,
1035        )
1036        .unwrap();
1037        assert_eq!(
1038            message.data[0],
1039            controllino_constants::COMMAND_UNSET_RELAY as u8
1040        );
1041        assert_eq!(message.data[1], config.controllino_relay.aux_pump2_id);
1042
1043        message = create_command_message(
1044            InternalCommand::SwitchOn(PeristalticPump1),
1045            &config.controllino_relay,
1046        )
1047        .unwrap();
1048        assert_eq!(
1049            message.data[0],
1050            controllino_constants::COMMAND_SET_RELAY as u8
1051        );
1052        assert_eq!(
1053            message.data[1],
1054            config.controllino_relay.peristaltic_pump1_id
1055        );
1056
1057        message = create_command_message(
1058            InternalCommand::SwitchOn(PeristalticPump2),
1059            &config.controllino_relay,
1060        )
1061        .unwrap();
1062        assert_eq!(
1063            message.data[0],
1064            controllino_constants::COMMAND_SET_RELAY as u8
1065        );
1066        assert_eq!(
1067            message.data[1],
1068            config.controllino_relay.peristaltic_pump2_id
1069        );
1070
1071        message = create_command_message(
1072            InternalCommand::SwitchOn(PeristalticPump3),
1073            &config.controllino_relay,
1074        )
1075        .unwrap();
1076        assert_eq!(
1077            message.data[0],
1078            controllino_constants::COMMAND_SET_RELAY as u8
1079        );
1080        assert_eq!(
1081            message.data[1],
1082            config.controllino_relay.peristaltic_pump3_id
1083        );
1084
1085        message = create_command_message(
1086            InternalCommand::SwitchOn(PeristalticPump4),
1087            &config.controllino_relay,
1088        )
1089        .unwrap();
1090        assert_eq!(
1091            message.data[0],
1092            controllino_constants::COMMAND_SET_RELAY as u8
1093        );
1094        assert_eq!(
1095            message.data[1],
1096            config.controllino_relay.peristaltic_pump4_id
1097        );
1098
1099        message =
1100            create_command_message(InternalCommand::SwitchOn(Feeder), &config.controllino_relay)
1101                .unwrap();
1102        assert_eq!(
1103            message.data[0],
1104            controllino_constants::COMMAND_SET_RELAY as u8
1105        );
1106        assert_eq!(message.data[1], config.controllino_relay.feeder_id);
1107
1108        // switching off all devices
1109        let message = create_command_message(
1110            InternalCommand::SwitchOff(Skimmer),
1111            &config.controllino_relay,
1112        )
1113        .unwrap();
1114        assert_eq!(
1115            message.data[0],
1116            controllino_constants::COMMAND_SET_RELAY as u8
1117        );
1118        assert_eq!(message.data[1], config.controllino_relay.skimmer_id);
1119
1120        let mut message = create_command_message(
1121            InternalCommand::SwitchOff(Ventilation),
1122            &config.controllino_relay,
1123        )
1124        .unwrap();
1125        assert_eq!(
1126            message.data[0],
1127            controllino_constants::COMMAND_UNSET_RELAY as u8
1128        );
1129        assert_eq!(message.data[1], config.controllino_relay.ventilation_id);
1130
1131        message = create_command_message(
1132            InternalCommand::SwitchOff(Heater),
1133            &config.controllino_relay,
1134        )
1135        .unwrap();
1136        assert_eq!(
1137            message.data[0],
1138            controllino_constants::COMMAND_UNSET_RELAY as u8
1139        );
1140        assert_eq!(message.data[1], config.controllino_relay.heater_id);
1141
1142        message = create_command_message(
1143            InternalCommand::SwitchOff(MainPump1),
1144            &config.controllino_relay,
1145        )
1146        .unwrap();
1147        assert_eq!(
1148            message.data[0],
1149            controllino_constants::COMMAND_SET_RELAY as u8
1150        );
1151        assert_eq!(message.data[1], config.controllino_relay.main_pump1_id);
1152
1153        message = create_command_message(
1154            InternalCommand::SwitchOff(MainPump2),
1155            &config.controllino_relay,
1156        )
1157        .unwrap();
1158        assert_eq!(
1159            message.data[0],
1160            controllino_constants::COMMAND_SET_RELAY as u8
1161        );
1162        assert_eq!(message.data[1], config.controllino_relay.main_pump2_id);
1163
1164        message = create_command_message(
1165            InternalCommand::SwitchOff(AuxPump1),
1166            &config.controllino_relay,
1167        )
1168        .unwrap();
1169        assert_eq!(
1170            message.data[0],
1171            controllino_constants::COMMAND_SET_RELAY as u8
1172        );
1173        assert_eq!(message.data[1], config.controllino_relay.aux_pump1_id);
1174
1175        message = create_command_message(
1176            InternalCommand::SwitchOff(AuxPump2),
1177            &config.controllino_relay,
1178        )
1179        .unwrap();
1180        assert_eq!(
1181            message.data[0],
1182            controllino_constants::COMMAND_SET_RELAY as u8
1183        );
1184        assert_eq!(message.data[1], config.controllino_relay.aux_pump2_id);
1185
1186        message = create_command_message(
1187            InternalCommand::SwitchOff(PeristalticPump1),
1188            &config.controllino_relay,
1189        )
1190        .unwrap();
1191        assert_eq!(
1192            message.data[0],
1193            controllino_constants::COMMAND_UNSET_RELAY as u8
1194        );
1195        assert_eq!(
1196            message.data[1],
1197            config.controllino_relay.peristaltic_pump1_id
1198        );
1199
1200        message = create_command_message(
1201            InternalCommand::SwitchOff(PeristalticPump2),
1202            &config.controllino_relay,
1203        )
1204        .unwrap();
1205        assert_eq!(
1206            message.data[0],
1207            controllino_constants::COMMAND_UNSET_RELAY as u8
1208        );
1209        assert_eq!(
1210            message.data[1],
1211            config.controllino_relay.peristaltic_pump2_id
1212        );
1213
1214        message = create_command_message(
1215            InternalCommand::SwitchOff(PeristalticPump3),
1216            &config.controllino_relay,
1217        )
1218        .unwrap();
1219        assert_eq!(
1220            message.data[0],
1221            controllino_constants::COMMAND_UNSET_RELAY as u8
1222        );
1223        assert_eq!(
1224            message.data[1],
1225            config.controllino_relay.peristaltic_pump3_id
1226        );
1227
1228        message = create_command_message(
1229            InternalCommand::SwitchOff(PeristalticPump4),
1230            &config.controllino_relay,
1231        )
1232        .unwrap();
1233        assert_eq!(
1234            message.data[0],
1235            controllino_constants::COMMAND_UNSET_RELAY as u8
1236        );
1237        assert_eq!(
1238            message.data[1],
1239            config.controllino_relay.peristaltic_pump4_id
1240        );
1241
1242        message = create_command_message(
1243            InternalCommand::SwitchOff(Feeder),
1244            &config.controllino_relay,
1245        )
1246        .unwrap();
1247        assert_eq!(
1248            message.data[0],
1249            controllino_constants::COMMAND_UNSET_RELAY as u8
1250        );
1251        assert_eq!(message.data[1], config.controllino_relay.feeder_id);
1252    }
1253
1254    #[test]
1255    // Tests the message creation for pulse command.
1256    // Pulse command is only permitted for a subset of the devices (e.g., peristaltic pumps).
1257    pub fn test_create_message_pulse() {
1258        let config: ConfigData =
1259            read_config_file("/config/aquarium_control_test_simulator.toml".to_string()).unwrap();
1260
1261        let mut error_message = create_command_message(
1262            InternalCommand::Pulse(Skimmer, 1000),
1263            &config.controllino_relay,
1264        );
1265        assert!(matches!(
1266            error_message,
1267            Err(RelayError::PulseNotAllowedForDevice(_, Skimmer))
1268        ));
1269
1270        error_message = create_command_message(
1271            InternalCommand::Pulse(Ventilation, 1000),
1272            &config.controllino_relay,
1273        );
1274        assert!(matches!(
1275            error_message,
1276            Err(RelayError::PulseNotAllowedForDevice(_, Ventilation))
1277        ));
1278
1279        error_message = create_command_message(
1280            InternalCommand::Pulse(Heater, 1000),
1281            &config.controllino_relay,
1282        );
1283        assert!(matches!(
1284            error_message,
1285            Err(RelayError::PulseNotAllowedForDevice(_, Heater))
1286        ));
1287
1288        error_message = create_command_message(
1289            InternalCommand::Pulse(MainPump1, 1000),
1290            &config.controllino_relay,
1291        );
1292        assert!(matches!(
1293            error_message,
1294            Err(RelayError::PulseNotAllowedForDevice(_, MainPump1))
1295        ));
1296
1297        error_message = create_command_message(
1298            InternalCommand::Pulse(MainPump2, 1000),
1299            &config.controllino_relay,
1300        );
1301        assert!(matches!(
1302            error_message,
1303            Err(RelayError::PulseNotAllowedForDevice(_, MainPump2))
1304        ));
1305
1306        error_message = create_command_message(
1307            InternalCommand::Pulse(AuxPump1, 1000),
1308            &config.controllino_relay,
1309        );
1310        assert!(matches!(
1311            error_message,
1312            Err(RelayError::PulseNotAllowedForDevice(_, AuxPump1))
1313        ));
1314
1315        error_message = create_command_message(
1316            InternalCommand::Pulse(AuxPump2, 1000),
1317            &config.controllino_relay,
1318        );
1319        assert!(matches!(
1320            error_message,
1321            Err(RelayError::PulseNotAllowedForDevice(_, AuxPump2))
1322        ));
1323
1324        let mut message = create_command_message(
1325            InternalCommand::Pulse(PeristalticPump1, 1000),
1326            &config.controllino_relay,
1327        )
1328        .unwrap();
1329
1330        assert_eq!(
1331            message.data[0],
1332            controllino_constants::COMMAND_PULSE_RELAY as u8
1333        );
1334        assert_eq!(
1335            message.data[1],
1336            config.controllino_relay.peristaltic_pump1_id
1337        );
1338
1339        message = create_command_message(
1340            InternalCommand::Pulse(PeristalticPump2, 1000),
1341            &config.controllino_relay,
1342        )
1343        .unwrap();
1344        assert_eq!(
1345            message.data[0],
1346            controllino_constants::COMMAND_PULSE_RELAY as u8
1347        );
1348        assert_eq!(
1349            message.data[1],
1350            config.controllino_relay.peristaltic_pump2_id
1351        );
1352
1353        message = create_command_message(
1354            InternalCommand::Pulse(PeristalticPump3, 1000),
1355            &config.controllino_relay,
1356        )
1357        .unwrap();
1358        assert_eq!(
1359            message.data[0],
1360            controllino_constants::COMMAND_PULSE_RELAY as u8
1361        );
1362        assert_eq!(
1363            message.data[1],
1364            config.controllino_relay.peristaltic_pump3_id
1365        );
1366
1367        message = create_command_message(
1368            InternalCommand::Pulse(PeristalticPump4, 1000),
1369            &config.controllino_relay,
1370        )
1371        .unwrap();
1372        assert_eq!(
1373            message.data[0],
1374            controllino_constants::COMMAND_PULSE_RELAY as u8
1375        );
1376        assert_eq!(
1377            message.data[1],
1378            config.controllino_relay.peristaltic_pump4_id
1379        );
1380
1381        error_message = create_command_message(
1382            InternalCommand::Pulse(Feeder, 1000),
1383            &config.controllino_relay,
1384        );
1385        assert!(matches!(
1386            error_message,
1387            Err(RelayError::PulseNotAllowedForDevice(_, Feeder))
1388        ));
1389    }
1390
1391    #[cfg(feature = "controllino_hw")]
1392    #[test]
1393    // Test case actuates on real hardware.
1394    // Execution of this test case requires serial port connection to the Controllino device.
1395    pub fn test_controllino_simulate_feed_profile() {
1396        let sleep_duration_500_millis = Duration::from_millis(500);
1397        let sleep_duration_8_secs = Duration::from_secs(8);
1398        let sleep_duration_6_secs = Duration::from_secs(6);
1399        let spin_sleeper = SpinSleeper::default();
1400
1401        let config: ConfigData = read_config_file(get_platform_specific_configuration_file());
1402        let controllino = Controllino::new(config.relay_manager);
1403
1404        let config2: ConfigData = read_config_file(get_platform_specific_configuration_file());
1405
1406        let serial_port: Box<dyn SerialPort>;
1407        serial_port = match controllino.serial_port_opt.as_ref().unwrap().try_clone() {
1408            Ok(c) => c,
1409            Err(_) => {
1410                panic!("Controllino: Error occurred when trying to clone serial port.");
1411            }
1412        };
1413
1414        let mut actuator = ControllinoActuateHardware;
1415
1416        let expect_info =
1417            "Controllino test_switch_on_off_pulse failed cloning serial port interface.";
1418
1419        // Request Controllino to switch on and off all relays sequentially
1420        assert_eq!(
1421            actuator.actuate(InternalCommand::SwitchOff(MainPump1),),
1422            Ok(())
1423        );
1424        spin_sleeper.sleep(sleep_duration_500_millis);
1425        assert_eq!(
1426            actuator.actuate(InternalCommand::SwitchOff(MainPump2),),
1427            Ok(())
1428        );
1429        spin_sleeper.sleep(sleep_duration_500_millis);
1430        assert_eq!(
1431            actuator.actuate(InternalCommand::SwitchOff(AuxPump1),),
1432            Ok(())
1433        );
1434        spin_sleeper.sleep(sleep_duration_8_secs);
1435        assert_eq!(actuator.actuate(InternalCommand::SwitchOn(Feeder),), Ok(()));
1436        spin_sleeper.sleep(sleep_duration_6_secs);
1437        assert_eq!(
1438            actuator.actuate(InternalCommand::SwitchOff(Feeder),),
1439            Ok(())
1440        );
1441        spin_sleeper.sleep(sleep_duration_8_secs);
1442        assert_eq!(actuator.actuate(InternalCommand::SwitchOn(Feeder),), Ok(()));
1443        spin_sleeper.sleep(sleep_duration_6_secs);
1444        assert_eq!(
1445            actuator.actuate(InternalCommand::SwitchOff(Feeder),),
1446            Ok(())
1447        );
1448        spin_sleeper.sleep(sleep_duration_8_secs);
1449        assert_eq!(
1450            actuator.actuate(InternalCommand::SwitchOn(MainPump1),),
1451            Ok(())
1452        );
1453        spin_sleeper.sleep(sleep_duration_500_millis);
1454        assert_eq!(
1455            actuator.actuate(InternalCommand::SwitchOn(MainPump2),),
1456            Ok(())
1457        );
1458        spin_sleeper.sleep(sleep_duration_500_millis);
1459        assert_eq!(
1460            actuator.actuate(InternalCommand::SwitchOn(AuxPump1),),
1461            Ok(())
1462        );
1463    }
1464}