aquarium_control/sensors/
gpio_handler.rs

1/* Copyright 2024 Uwe Martin
2
3Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
5The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
7THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8*/
9#[cfg(not(test))]
10use log::error;
11use std::collections::HashSet;
12
13use crate::sensors::gpio_handler_config::GpioHandlerConfig;
14use thiserror::Error;
15
16cfg_if::cfg_if! {
17    if #[cfg(all(target_os = "linux", feature = "target_hw"))] {
18            use rppal::gpio::{Gpio};
19    }
20}
21
22// create a stub version of the type to allow build on non-target platforms
23#[cfg(not(all(target_os = "linux", feature = "target_hw")))]
24pub type Gpio = u64;
25
26/// Minimum permissible GPIO value. The configuration is checked against this value.
27pub const GPIO_MIN: u8 = 3;
28
29/// Maximum permissible GPIO value. The configuration is checked against this value.
30pub const GPIO_MAX: u8 = 28;
31
32/// Contains error definition for GpioHandler
33#[derive(Error, Debug)]
34pub enum GpioHandlerError {
35    /// Configuration contains invalid GPIO pin mapping.
36    #[error("Configuration contains invalid GPIO pin mapping: {0} is out of range")]
37    PinOutsideRange(u8),
38
39    /// Found duplicate GPIO values in configuration.
40    #[error("Found duplicate GPIO values in configuration: {0} is not unique.")]
41    PinDuplicateValue(u8),
42
43    /// Could not open interface to GPIO.
44    #[cfg(all(target_os = "linux", feature = "target_hw"))]
45    #[error("Could not open interface to GPIO.")]
46    OpenInterfaceFailed {
47        #[source]
48        source: rppal::gpio::Error,
49    },
50
51    /// This error is triggered, When running either on different OS or without target hardware.
52    /// Defensive programming: This prevents the startup of the application.
53    #[cfg(any(not(target_os = "linux"), not(feature = "target_hw")))]
54    #[error("Usage of hardware not supported on this platform.")]
55    PlatformNotSupported,
56}
57#[cfg_attr(doc, aquamarine::aquamarine)]
58/// Contains the configuration and the implementation for the GPIO handler.
59pub struct GpioHandler {
60    #[cfg(all(target_os = "linux", feature = "target_hw"))]
61    /// GPIO interface wrapped in Option
62    pub gpio_lib_handle_opt: Option<Gpio>,
63}
64
65impl GpioHandler {
66    /// Checks if the GPIO pin configuration is valid and free from conflicts.
67    ///
68    /// This function performs two essential validation checks on the configured GPIO pins:
69    /// 1.  **Range Check**: Ensures all pins fall within the permissible operating
70    ///     range defined by `GPIO_MIN` and `GPIO_MAX`.
71    /// 2.  **Uniqueness Check**: Verifies that all assigned pins are distinct,
72    ///     preventing multiple devices from being configured to the same physical pin.
73    ///
74    /// # Arguments
75    /// * `config` - The `GpioHandlerConfig` struct containing the GPIO pin assignments.
76    ///
77    /// # Returns
78    /// An empty `Result` (`Ok(())`) if all GPIO pins are valid.
79    ///
80    /// # Errors
81    /// Returns a `GpioHandlerError` if any validation check fails:
82    /// - `GpioHandlerError::PinOutsideRange`: If any configured GPIO pin is
83    ///   outside the valid range (e.g., less than or equal to 3, or greater than
84    ///   or equal to 28).
85    /// - `GpioHandlerError::PinDuplicateValue`: If the same GPIO pin is assigned
86    ///   to more than one device.
87    pub fn check_valid_gpio_pin_config(config: &GpioHandlerConfig) -> Result<(), GpioHandlerError> {
88        // check if pin ids are in valid range and if they are distinct
89        const VALID_GPIO_RANGE: std::ops::Range<u8> = GPIO_MIN..(GPIO_MAX + 1);
90
91        let mut seen_values = HashSet::new();
92
93        let gpio_values = [
94            config.tank_level_switch,
95            config.dht22_io,
96            config.dht22_vcc,
97            config.skimmer,
98            config.main_pump1,
99            config.main_pump2,
100            config.aux_pump1,
101            config.aux_pump2,
102            config.feeder,
103            config.refill_pump,
104            config.heater,
105            config.ventilation,
106            config.peristaltic_pump1,
107            config.peristaltic_pump2,
108            config.peristaltic_pump3,
109            config.peristaltic_pump4,
110        ];
111
112        for &value in &gpio_values {
113            if !VALID_GPIO_RANGE.contains(&value) {
114                return Err(GpioHandlerError::PinOutsideRange(value));
115            }
116            if !seen_values.insert(value) {
117                return Err(GpioHandlerError::PinDuplicateValue(value));
118            }
119        }
120
121        Ok(())
122    }
123
124    #[cfg(all(target_os = "linux", feature = "target_hw"))]
125    /// Creates a new `GpioHandler` instance for the target hardware platform (Linux).
126    ///
127    /// This constructor initializes the GPIO handler. It first validates the pin
128    /// configuration using `check_valid_gpio_pin_config`. If `use_simulator` is
129    /// false, it then attempts to acquire a handle to the system's GPIO interface
130    /// using `rppal`.
131    ///
132    /// # Arguments
133    /// * `config` - Configuration data for the GPIO handler, specifying the pins and
134    ///   simulation mode.
135    ///
136    /// # Returns
137    /// A `Result` containing an `Option<GpioHandler>` on success:
138    /// - `Ok(Some(GpioHandler))` with an active `Gpio` handle if `use_simulator` is `false`.
139    /// - `Ok(Some(GpioHandler))` with `None` for the handle if `use_simulator` is `true`.
140    ///
141    /// # Errors
142    /// Returns a `GpioHandlerError` if initialization fails:
143    /// - `GpioHandlerError::PinOutsideRange` or `PinDuplicateValue`: If the pin
144    ///   configuration is invalid (propagated from `check_valid_gpio_pin_config`).
145    /// - `GpioHandlerError::OpenInterfaceFailed`: If `use_simulator` is `false` and
146    ///   the function fails to open the underlying GPIO interface (e.g., due to
147    ///   permission issues or hardware problems).
148    pub fn new(config: GpioHandlerConfig) -> Result<Option<GpioHandler>, GpioHandlerError> {
149        let use_simulator = config.use_simulator; // create copy because the struct is moved
150
151        // check if GPIO pin configuration is valid
152        Self::check_valid_gpio_pin_config(&config)?;
153
154        if !use_simulator {
155            // open interface to the GPIO library
156            let gpio_lib_handle =
157                Gpio::new().map_err(|e| GpioHandlerError::OpenInterfaceFailed { source: e })?;
158
159            Ok(Some(GpioHandler {
160                gpio_lib_handle_opt: Some(gpio_lib_handle),
161            }))
162        } else {
163            Ok(Some(GpioHandler {
164                gpio_lib_handle_opt: None,
165            }))
166        }
167    }
168
169    #[cfg(any(not(target_os = "linux"), not(feature = "target_hw")))]
170    #[allow(unused)]
171    /// Creates a new `GpioHandler` instance for development or non-target platforms.
172    ///
173    /// This constructor is activated when compiling for a non-Linux OS or when the
174    /// `target_hw` feature is disabled. It allows the application to run in a
175    /// simulated mode without requiring real GPIO hardware.
176    ///
177    /// # Arguments
178    /// * `config` - Configuration data for the GPIO handler.
179    ///
180    /// # Returns
181    /// A `Result` containing `Ok(None)` if the configuration is valid and `use_simulator`
182    /// is `true`, indicating that no hardware handle is needed.
183    ///
184    /// # Errors
185    /// Returns a `GpioHandlerError` if initialization fails:
186    /// - `GpioHandlerError::PlatformNotSupported`: If `use_simulator` is `false`,
187    ///   as hardware access is not possible on the current platform.
188    /// - `GpioHandlerError::PinOutsideRange` or `PinDuplicateValue`: If the pin
189    ///   configuration is invalid (propagated from `check_valid_gpio_pin_config`)
190    pub fn new(config: GpioHandlerConfig) -> Result<Option<GpioHandler>, GpioHandlerError> {
191        if !config.use_simulator {
192            return Err(GpioHandlerError::PlatformNotSupported);
193        }
194
195        Self::check_valid_gpio_pin_config(&config)?;
196
197        Ok(None)
198    }
199}
200
201pub mod tests {
202    #[allow(unused_imports)]
203    use crate::sensors::gpio_handler::{GpioHandler, GpioHandlerError};
204    #[allow(unused_imports)]
205    use crate::utilities::config::{read_config_file, ConfigData};
206
207    #[test]
208    pub fn test_valid_gpio_config() {
209        let config: ConfigData =
210            read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
211        // The `new` function should succeed for a valid configuration.
212        assert!(GpioHandler::new(config.gpio_handler).is_ok());
213    }
214
215    #[test]
216    pub fn test_invalid_gpio_config_low() {
217        let mut config: ConfigData =
218            read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
219
220        config.gpio_handler.dht22_io = 2; // Value is at the boundary, which is invalid.
221
222        let result = GpioHandler::new(config.gpio_handler);
223        assert!(result.is_err());
224        // Check that we get the specific error we expect.
225        assert!(matches!(result, Err(GpioHandlerError::PinOutsideRange(_))));
226    }
227    #[test]
228    pub fn test_invalid_gpio_config_duplicate() {
229        let mut config: ConfigData =
230            read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
231
232        let duplicate_pin = 5;
233        config.gpio_handler.dht22_io = duplicate_pin;
234        config.gpio_handler.tank_level_switch = duplicate_pin;
235
236        let result = GpioHandler::new(config.gpio_handler);
237        assert!(result.is_err());
238        assert!(matches!(
239            result,
240            Err(GpioHandlerError::PinDuplicateValue(_))
241        ));
242    }
243
244    #[test]
245    // This check is platform-agnostic, so the cfg attribute isn't needed.
246    pub fn test_invalid_gpio_config_high() {
247        let mut config: ConfigData =
248            read_config_file("/config/aquarium_control_test_generic.toml".to_string()).unwrap();
249
250        config.gpio_handler.dht22_io = 40; // Value is outside the valid range.
251
252        let result = GpioHandler::new(config.gpio_handler);
253        assert!(result.is_err());
254        assert!(matches!(result, Err(GpioHandlerError::PinOutsideRange(40))));
255    }
256
257    mod commissioning_prep {
258        cfg_if::cfg_if! {
259            if #[cfg(all(feature = "commissioning_prep", feature = "target_hw", target_os = "linux", test))] {
260                use crate::config::ConfigData;
261                use crate::read_config_file;
262                use crate::GpioHandler;
263
264                use spin_sleep::SpinSleeper;
265                use std::time::Duration;
266            }
267        }
268
269        #[test]
270        #[cfg(all(
271            feature = "commissioning_prep",
272            feature = "target_hw",
273            target_os = "linux",
274            test
275        ))]
276        // for development purposes, visualize the tank level switch pin state with 2 Hz
277        pub fn test_show_tank_level_switch() {
278            let config: ConfigData = read_config_file("/config/aquarium_control.toml".to_string());
279            let (_, tank_level_switch_input_pin_opt, _dht_opt) =
280                GpioHandler::new(config.gpio_handler, config.dht);
281
282            let tank_level_switch_input_pin = match tank_level_switch_input_pin_opt {
283                Some(c) => c,
284                None => {
285                    panic!("Could not initialize tank level switch input pin.");
286                }
287            };
288
289            let sleep_time = Duration::from_millis(500);
290            let spin_sleeper = SpinSleeper::default();
291
292            loop {
293                println!(
294                    "Tank Level switch input pin is {}",
295                    match tank_level_switch_input_pin.is_high() {
296                        true => "HIGH",
297                        false => "LOW",
298                    }
299                );
300
301                spin_sleeper.sleep(sleep_time);
302            }
303        }
304    }
305}