1use 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#[allow(unused)]
26pub mod controllino_constants {
27 pub const MESSAGE_SIZE: usize = 8;
29
30 pub const COMMAND_SET_RELAY: char = 'S';
32
33 pub const COMMAND_UNSET_RELAY: char = 'U';
35
36 pub const COMMAND_PULSE_RELAY: char = 'P';
38
39 pub const COMMAND_GET_RELAY_STATUS: char = 'G';
41
42 pub const COMMAND_HEARTBEAT: char = 'H';
44
45 pub const COMMAND_SET_DIGITAL_OUT: char = 'D';
47
48 pub const COMMAND_UNSET_DIGITAL_OUT: char = 'J';
50
51 pub const COMMAND_GET_DIGITAL_OUT_STATUS: char = 'G';
53
54 pub const SLEEP_AFTER_DTR_RESET_MILLIS: u64 = 1000;
56
57 pub const SLEEP_AFTER_CLEAR_BUFFER_MILLIS: u64 = 2000;
59}
60
61fn 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#[derive(Error, Debug)]
99pub enum ActuateControllinoError {
100 #[error("Relay ID for Controllino ({0}) is outside the allowed range ({1} ... {2}).")]
102 RelayIdOutsideRange(u8, u8, u8),
103
104 #[error("Found a duplicate value ({0}) in definition of relay IDs.")]
106 RelayIdDuplicate(u8),
107
108 #[error("Port name for communication with Controllino is empty.")]
110 EmptyPortName,
111
112 #[error("Failed to open serial port for Controllino.")]
114 SerialPortOpenFailed {
115 #[source]
116 source: serialport::Error,
117 },
118
119 #[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 #[error("Failed to clear DTR of serial port communication.")]
132 ClearDTRFailed {
133 #[source]
134 source: serialport::Error,
135 },
136
137 #[error("Failed to clear buffer of serial port communication.")]
139 ClearBufferFailed {
140 #[source]
141 source: serialport::Error,
142 },
143}
144
145pub struct ActuateControllino {
147 config: ActuateControllinoConfig,
149
150 pub spin_sleeper: SpinSleeper,
153
154 pub controllino_processing_duration: Duration,
157
158 pub serial_port_opt: Option<Box<dyn SerialPort>>,
161}
162
163fn 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 (controllino_constants::COMMAND_SET_RELAY, relay_id as u8, 0)
192 }
193 InternalCommand::UnsetRelay(relay_id) => {
194 (
196 controllino_constants::COMMAND_UNSET_RELAY,
197 relay_id as u8,
198 0,
199 )
200 }
201 InternalCommand::PulseRelay(relay_id, duration_millis) => {
202 (
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
228fn create_heartbeat_message() -> ControllinoMessage {
237 let message_content =
238 ControllinoMessageContent::new(controllino_constants::COMMAND_HEARTBEAT, 0, 0);
239 ControllinoMessage::new(message_content)
240}
241
242pub 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 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 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 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 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 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 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 self.spin_sleeper
383 .sleep(self.controllino_processing_duration);
384
385 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 _ => { }
394 }
395
396 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 fn get_heartbeat_interval_seconds(&self) -> Option<u64> {
446 Some(self.config.heartbeat_interval_seconds)
447 }
448
449 fn heartbeat(&mut self) -> Result<(), RelayError> {
459 if let Some(serial_port) = self.serial_port_opt.as_mut() {
460 let message = create_heartbeat_message();
462
463 self.spin_sleeper
464 .sleep(self.controllino_processing_duration);
465
466 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 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
530fn 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 serial_port
572 .write_data_terminal_ready(false)
573 .map_err(|e| ActuateControllinoError::ClearDTRFailed { source: e })?;
574
575 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 serial_port
582 .clear(ClearBuffer::All)
583 .map_err(|e| ActuateControllinoError::ClearBufferFailed { source: e })?;
584
585 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 pub fn check_valid_relay_id_config(
614 config: &ActuateControllinoConfig,
615 ) -> Result<(), ActuateControllinoError> {
616 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 pub fn new(
668 config: ActuateControllinoConfig,
669 ) -> Result<ActuateControllino, ActuateControllinoError> {
670 let spin_sleeper = SpinSleeper::default();
671
672 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 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 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 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 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 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 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 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 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 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 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 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}