aquarium_control/relays/
actuate_gpio.rs

1/* Copyright 2025 Uwe Martin
2
3Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
5The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
7THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8*/
9
10cfg_if::cfg_if! {
11    if #[cfg(all(target_os = "linux", feature = "target_hw"))] {
12        use log::warn;
13        use crate::relays::device_relay_logic::get_relay_command_gpio;
14        use crate::relays::relay_error::RelayError;
15        use crate::relays::relay_manager::RelayActuationTrait;
16        use crate::utilities::channel_content::InternalCommand;
17        use crate::sensors::gpio_handler_config::GpioHandlerConfig;
18        use rppal::gpio::{Gpio, OutputPin};
19        use crate::utilities::channel_content::AquariumDevice;
20    }
21}
22
23#[cfg(all(target_os = "linux", feature = "target_hw"))]
24/// Contains configuration for actuation of relays via GPIO
25pub struct ActuateGpioConfig {
26    pub gpio_skimmer: u8,
27    pub gpio_main_pump1: u8,
28    pub gpio_main_pump2: u8,
29    pub gpio_aux_pump1: u8,
30    pub gpio_aux_pump2: u8,
31    pub gpio_feeder: u8,
32    pub gpio_refill_pump: u8,
33    pub gpio_heater: u8,
34    pub gpio_ventilation: u8,
35    pub gpio_peristaltic_pump1: u8,
36    pub gpio_peristaltic_pump2: u8,
37    pub gpio_peristaltic_pump3: u8,
38    pub gpio_peristaltic_pump4: u8,
39}
40
41#[cfg(all(target_os = "linux", feature = "target_hw"))]
42impl ActuateGpioConfig {
43    /// Creates a new `ActuateGpioConfig` by mapping GPIO pins from a `GpioHandlerConfig`.
44    ///
45    /// This constructor is used to extract and organize the specific GPIO pin assignments
46    /// for each aquarium device from a more general GPIO configuration structure.
47    /// It's used during the application's initialization exclusively on Linux systems
48    /// with the target hardware.
49    ///
50    /// # Arguments
51    /// * `gpio_handler_config` - A reference to the `GpioHandlerConfig` struct, which contains
52    ///   the raw GPIO pins for various devices.
53    ///
54    /// # Returns
55    /// A new `ActuateGpioConfig` struct with device-specific GPIO pin assignments.
56    pub fn new(gpio_handler_config: &GpioHandlerConfig) -> ActuateGpioConfig {
57        ActuateGpioConfig {
58            gpio_skimmer: gpio_handler_config.skimmer,
59            gpio_main_pump1: gpio_handler_config.main_pump1,
60            gpio_main_pump2: gpio_handler_config.main_pump2,
61            gpio_aux_pump1: gpio_handler_config.aux_pump1,
62            gpio_aux_pump2: gpio_handler_config.aux_pump2,
63            gpio_feeder: gpio_handler_config.feeder,
64            gpio_refill_pump: gpio_handler_config.refill_pump,
65            gpio_heater: gpio_handler_config.heater,
66            gpio_ventilation: gpio_handler_config.ventilation,
67            gpio_peristaltic_pump1: gpio_handler_config.peristaltic_pump1,
68            gpio_peristaltic_pump2: gpio_handler_config.peristaltic_pump2,
69            gpio_peristaltic_pump3: gpio_handler_config.peristaltic_pump3,
70            gpio_peristaltic_pump4: gpio_handler_config.peristaltic_pump4,
71        }
72    }
73}
74
75#[cfg(all(target_os = "linux", feature = "target_hw"))]
76/// Contains trait implementation for actuation of relays via GPIO
77pub struct ActuateGpio {
78    #[cfg(all(target_os = "linux", feature = "target_hw"))]
79    /// GPIO of the protein skimmer
80    pub output_pin_skimmer: OutputPin,
81
82    #[cfg(all(target_os = "linux", feature = "target_hw"))]
83    /// GPIO of the main pump #1
84    pub output_pin_main_pump1: OutputPin,
85
86    #[cfg(all(target_os = "linux", feature = "target_hw"))]
87    /// GPIO of the main pump #2
88    pub output_pin_main_pump2: OutputPin,
89
90    #[cfg(all(target_os = "linux", feature = "target_hw"))]
91    /// GPIO of the auxiliary pump #1
92    pub output_pin_aux_pump1: OutputPin,
93
94    #[cfg(all(target_os = "linux", feature = "target_hw"))]
95    /// GPIO of the auxiliary pump #2
96    pub output_pin_aux_pump2: OutputPin,
97
98    #[cfg(all(target_os = "linux", feature = "target_hw"))]
99    /// GPIO of the feeder
100    pub output_pin_feeder: OutputPin,
101
102    #[cfg(all(target_os = "linux", feature = "target_hw"))]
103    /// GPIO of the refill pump
104    pub output_pin_refill_pump: OutputPin,
105
106    #[cfg(all(target_os = "linux", feature = "target_hw"))]
107    /// GPIO of the heater
108    pub output_pin_heater: OutputPin,
109
110    #[cfg(all(target_os = "linux", feature = "target_hw"))]
111    /// GPIO of the surface ventilation
112    pub output_pin_ventilation: OutputPin,
113
114    #[cfg(all(target_os = "linux", feature = "target_hw"))]
115    /// GPIO of the peristaltic pump #1
116    pub output_pin_peristaltic_pump1: OutputPin,
117
118    #[cfg(all(target_os = "linux", feature = "target_hw"))]
119    /// GPIO of the peristaltic pump #2
120    pub output_pin_peristaltic_pump2: OutputPin,
121
122    #[cfg(all(target_os = "linux", feature = "target_hw"))]
123    /// GPIO of the peristaltic pump #3
124    pub output_pin_peristaltic_pump3: OutputPin,
125
126    #[cfg(all(target_os = "linux", feature = "target_hw"))]
127    /// GPIO of the peristaltic pump #4
128    pub output_pin_peristaltic_pump4: OutputPin,
129}
130
131#[cfg(all(target_os = "linux", feature = "target_hw"))]
132/// Acquires and configures a specific GPIO pin as an output pin.
133///
134/// This private helper function attempts to get access to a GPIO pin by its number
135/// and then sets its mode to output. This is a critical step for controlling
136/// hardware devices connected to the GPIOs.
137///
138/// # Arguments
139/// * `gpio` - A reference to the `Gpio` instance, which provides access to the GPIO interface.
140/// * `gpio_number` - The physical GPIO pin to configure.
141/// * `device_name` - A descriptive name of the device connected to this pin (used for error logging).
142///
143/// # Returns
144/// A `Result` containing a new `OutputPin` instance on success, ready to be used for setting the pin's state.
145///
146/// # Errors
147/// Returns a `RelayError::GpioGetPinFailure` if the underlying `rppal` library fails
148/// to access the pin. This can happen if the pin number is invalid for the hardware,
149/// if the pin is already in use by another process, or due to insufficient permissions.
150pub fn get_output_pin(
151    gpio: &Gpio,
152    gpio_number: u8,
153    device_name: &str,
154) -> Result<OutputPin, RelayError> {
155    gpio.get(gpio_number)
156        .map(|pin| pin.into_output())
157        .map_err(|e| RelayError::GpioGetPinFailure {
158            location: module_path!().to_string(),
159            pin_number: gpio_number,
160            device: device_name.to_string(),
161            source: e,
162        })
163}
164
165#[cfg(all(target_os = "linux", feature = "target_hw"))]
166impl ActuateGpio {
167    #[cfg(all(target_os = "linux", feature = "target_hw"))]
168    /// Creates a new `ActuateGpio` instance by configuring and acquiring all necessary GPIO output pins.
169    ///
170    /// This constructor is responsible for initializing direct GPIO hardware control.
171    /// It iterates through the `ActuateGpioConfig` to get each device's assigned GPIO pin,
172    /// then acquires and sets up each pin as an `OutputPin` using the `rppal` library.
173    ///
174    /// # Arguments
175    /// * `gpio` - A `Gpio` instance, which is the entry point for accessing the GPIO interface
176    ///   on a Linux system (e.g., Raspberry Pi).
177    /// * `config` - An `ActuateGpioConfig` struct, containing the mapping of aquarium devices to their
178    ///   specific GPIO pins.
179    ///
180    /// # Returns
181    /// A `Result` containing a new `ActuateGpio` struct on success, holding all the configured `OutputPin`s.
182    ///
183    /// # Errors
184    /// Returns a `RelayError` if any of the underlying calls to `get_output_pin` fail.
185    /// This indicates a critical hardware or configuration error that prevents the
186    /// application from controlling the hardware correctly (e.g., invalid pin number,
187    /// permission issues).
188    pub fn new(gpio: Gpio, config: ActuateGpioConfig) -> Result<ActuateGpio, RelayError> {
189        let output_pin_skimmer = get_output_pin(&gpio, config.gpio_skimmer, "skimmer")?;
190        let output_pin_main_pump1 = get_output_pin(&gpio, config.gpio_main_pump1, "main pump 1")?;
191        let output_pin_main_pump2 = get_output_pin(&gpio, config.gpio_main_pump2, "main pump 2")?;
192        let output_pin_aux_pump1 = get_output_pin(&gpio, config.gpio_aux_pump1, "aux pump 1")?;
193        let output_pin_aux_pump2 = get_output_pin(&gpio, config.gpio_aux_pump2, "aux pump 2")?;
194        let output_pin_feeder = get_output_pin(&gpio, config.gpio_feeder, "feeder")?;
195        let output_pin_refill_pump = get_output_pin(&gpio, config.gpio_refill_pump, "refill pump")?;
196        let output_pin_heater = get_output_pin(&gpio, config.gpio_heater, "heater")?;
197        let output_pin_ventilation = get_output_pin(&gpio, config.gpio_ventilation, "ventilation")?;
198        let output_pin_peristaltic_pump1 =
199            get_output_pin(&gpio, config.gpio_peristaltic_pump1, "peristaltic pump 1")?;
200        let output_pin_peristaltic_pump2 =
201            get_output_pin(&gpio, config.gpio_peristaltic_pump2, "peristaltic pump 2")?;
202        let output_pin_peristaltic_pump3 =
203            get_output_pin(&gpio, config.gpio_peristaltic_pump3, "peristaltic pump 3")?;
204        let output_pin_peristaltic_pump4 =
205            get_output_pin(&gpio, config.gpio_peristaltic_pump4, "peristaltic pump 4")?;
206
207        Ok(ActuateGpio {
208            output_pin_skimmer,
209            output_pin_main_pump1,
210            output_pin_main_pump2,
211            output_pin_aux_pump1,
212            output_pin_aux_pump2,
213            output_pin_feeder,
214            output_pin_refill_pump,
215            output_pin_heater,
216            output_pin_ventilation,
217            output_pin_peristaltic_pump1,
218            output_pin_peristaltic_pump2,
219            output_pin_peristaltic_pump3,
220            output_pin_peristaltic_pump4,
221        })
222    }
223}
224
225#[cfg(all(target_os = "linux", feature = "target_hw"))]
226impl RelayActuationTrait for ActuateGpio {
227    /// Actuates a relay by setting the state of its corresponding GPIO pin.
228    ///
229    /// This function translates `SwitchOn` and `SwitchOff` commands for specific aquarium devices
230    /// into direct GPIO pin manipulations (`set_low()` or `set_high()`). It applies the correct
231    /// relay logic (e.g., whether `set_low()` means "on" or "off" for a given device)
232    /// by calling `get_relay_command_gpio`.
233    ///
234    /// This implementation is active only on Linux systems when the `target_hw` feature is enabled.
235    ///
236    /// # Arguments
237    /// * `internal_command` - The `InternalCommand` specifying the `AquariumDevice` and the desired state (`SwitchOn` or `SwitchOff`).
238    ///
239    /// # Returns
240    /// An empty `Result` (`Ok(())`) if the GPIO pin state was successfully set.
241    ///
242    /// # Errors
243    /// This function will return a `RelayError` if the command is not applicable:
244    /// - `RelayError::IrrelevantCommand`: If the `internal_command` is not `SwitchOn` or `SwitchOff` (e.g., `Pulse`, `RequestSignal`).
245    /// - `RelayError::PulseNotAllowedForDevice`: If a `Pulse` command is attempted, as this GPIO implementation does not support it.
246    fn actuate(&mut self, internal_command: &InternalCommand) -> Result<(), RelayError> {
247        match internal_command {
248            InternalCommand::SwitchOn(ref aquarium_device)
249            | InternalCommand::SwitchOff(ref aquarium_device) => {
250                match get_relay_command_gpio(internal_command, aquarium_device.clone()) {
251                    Ok(gpio_target_state) => match gpio_target_state {
252                        true => {
253                            #[cfg(all(target_os = "linux", feature = "target_hw"))]
254                            match aquarium_device {
255                                AquariumDevice::Skimmer => {
256                                    self.output_pin_skimmer.set_low();
257                                }
258                                AquariumDevice::MainPump1 => {
259                                    self.output_pin_main_pump1.set_low();
260                                }
261                                AquariumDevice::MainPump2 => {
262                                    self.output_pin_main_pump2.set_low();
263                                }
264                                AquariumDevice::AuxPump1 => {
265                                    self.output_pin_aux_pump1.set_low();
266                                }
267                                AquariumDevice::AuxPump2 => {
268                                    self.output_pin_aux_pump2.set_low();
269                                }
270                                AquariumDevice::Feeder => {
271                                    self.output_pin_feeder.set_low();
272                                }
273                                AquariumDevice::RefillPump => {
274                                    self.output_pin_refill_pump.set_low();
275                                }
276                                AquariumDevice::Heater => {
277                                    self.output_pin_heater.set_low();
278                                }
279                                AquariumDevice::Ventilation => {
280                                    self.output_pin_ventilation.set_low();
281                                }
282                                AquariumDevice::PeristalticPump1 => {
283                                    self.output_pin_peristaltic_pump1.set_low();
284                                }
285                                AquariumDevice::PeristalticPump2 => {
286                                    self.output_pin_peristaltic_pump2.set_low();
287                                }
288                                AquariumDevice::PeristalticPump3 => {
289                                    self.output_pin_peristaltic_pump3.set_low();
290                                }
291                                AquariumDevice::PeristalticPump4 => {
292                                    self.output_pin_peristaltic_pump4.set_low();
293                                }
294                            }
295                            Ok(())
296                        }
297                        false => {
298                            #[cfg(all(target_os = "linux", feature = "target_hw"))]
299                            match aquarium_device {
300                                AquariumDevice::Skimmer => {
301                                    self.output_pin_skimmer.set_high();
302                                }
303                                AquariumDevice::MainPump1 => {
304                                    self.output_pin_main_pump1.set_high();
305                                }
306                                AquariumDevice::MainPump2 => {
307                                    self.output_pin_main_pump2.set_high();
308                                }
309                                AquariumDevice::AuxPump1 => {
310                                    self.output_pin_aux_pump1.set_high();
311                                }
312                                AquariumDevice::AuxPump2 => {
313                                    self.output_pin_aux_pump2.set_high();
314                                }
315                                AquariumDevice::Feeder => {
316                                    self.output_pin_feeder.set_high();
317                                }
318                                AquariumDevice::RefillPump => {
319                                    self.output_pin_refill_pump.set_high();
320                                }
321                                AquariumDevice::Heater => {
322                                    self.output_pin_heater.set_high();
323                                }
324                                AquariumDevice::Ventilation => {
325                                    self.output_pin_ventilation.set_high();
326                                }
327                                AquariumDevice::PeristalticPump1 => {
328                                    self.output_pin_peristaltic_pump1.set_high();
329                                }
330                                AquariumDevice::PeristalticPump2 => {
331                                    self.output_pin_peristaltic_pump2.set_high();
332                                }
333                                AquariumDevice::PeristalticPump3 => {
334                                    self.output_pin_peristaltic_pump3.set_high();
335                                }
336                                AquariumDevice::PeristalticPump4 => {
337                                    self.output_pin_peristaltic_pump4.set_high();
338                                }
339                            }
340                            Ok(())
341                        }
342                    },
343                    Err(e) => Err(e),
344                }
345            }
346            _ => {
347                warn!(
348                    target: module_path!(),
349                    "ignoring irrelevant internal command {internal_command}."
350                );
351                Err(RelayError::IrrelevantCommand(
352                    module_path!().to_string(),
353                    internal_command.clone(),
354                ))
355            }
356        }
357    }
358
359    /// Returns the heartbeat interval, which is not applicable for GPIO actuation.
360    ///
361    /// GPIO-based relays do not require a keep-alive signal, so this implementation
362    /// returns `None`.
363    ///
364    /// # Returns
365    /// Always returns `None`.
366    fn get_heartbeat_interval_seconds(&self) -> Option<u64> {
367        None
368    }
369
370    /// Performs a heartbeat action, which is not applicable for GPIO actuation.
371    ///
372    /// This is a no-op for the GPIO implementation as there is no persistent
373    /// communication channel that needs to be kept alive.
374    ///
375    /// # Returns
376    /// Always returns `Ok(())`.
377    ///
378    /// # Errors
379    /// This function will never return an error.
380    fn heartbeat(&mut self) -> Result<(), RelayError> {
381        // do nothing
382        Ok(())
383    }
384
385    /// Flushes a communication buffer, which is not applicable for GPIO actuation.
386    ///
387    /// This is a no-op for the GPIO implementation as there is no serial communication
388    /// buffer to flush.
389    ///
390    /// # Returns
391    /// Always returns `Ok(())`.
392    ///
393    /// # Errors
394    /// This function will never return an error.
395    fn flush_buffer(&mut self) -> Result<(), RelayError> {
396        // do nothing
397        Ok(())
398    }
399}