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}