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}