hermes_five/devices/input/
analog.rs

1use std::fmt::{Display, Formatter};
2use std::sync::Arc;
3
4use parking_lot::RwLock;
5
6use crate::devices::input::{Input, InputEvent};
7use crate::devices::Device;
8use crate::errors::Error;
9use crate::hardware::Hardware;
10use crate::io::{IoProtocol, PinIdOrName, PinModeId};
11use crate::pause;
12use crate::utils::task;
13use crate::utils::{EventHandler, EventManager, State, TaskHandler};
14
15/// Represents an analog sensor of unspecified type: an [`Input`] [`Device`] that reads analog values
16/// from an ANALOG compatible pin.
17/// <https://docs.arduino.cc/built-in-examples/analog/AnalogInput>
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19#[derive(Clone, Debug)]
20pub struct AnalogInput {
21    // ########################################
22    // # Basics
23    /// The pin (id) of the [`Board`] used to read the analog value.
24    pin: u8,
25    /// The current AnalogInput state.
26    #[cfg_attr(feature = "serde", serde(with = "crate::devices::arc_rwlock_serde"))]
27    state: Arc<RwLock<u16>>,
28
29    // ########################################
30    // # Volatile utility data.
31    #[cfg_attr(feature = "serde", serde(skip))]
32    protocol: Box<dyn IoProtocol>,
33    /// Inner handler to the task running the button value check.
34    #[cfg_attr(feature = "serde", serde(skip))]
35    handler: Arc<RwLock<Option<TaskHandler>>>,
36    /// The event manager for the AnalogInput.
37    #[cfg_attr(feature = "serde", serde(skip))]
38    events: EventManager,
39}
40
41impl AnalogInput {
42    /// Creates an instance of an [`AnalogInput`] attached to a given board:
43    /// <https://docs.arduino.cc/built-in-examples/analog/AnalogInput/>
44    ///
45    /// # Errors
46    /// * `UnknownPin`: this function will bail an error if the AnalogInput pin does not exist for this board.
47    /// * `IncompatiblePin`: this function will bail an error if the AnalogInput pin does not support ANALOG mode.
48    pub fn new<T: Into<PinIdOrName>>(board: &dyn Hardware, analog_pin: T) -> Result<Self, Error> {
49        let pin = board.get_io().read().get_pin(analog_pin)?.clone();
50
51        let mut sensor = Self {
52            pin: pin.id,
53            state: Arc::new(RwLock::new(pin.value)),
54            protocol: board.get_protocol(),
55            handler: Arc::new(RwLock::new(None)),
56            events: Default::default(),
57        };
58
59        // Set pin mode to ANALOG.
60        sensor
61            .protocol
62            .set_pin_mode(sensor.pin, PinModeId::ANALOG)?;
63
64        // Start reporting.
65        sensor.protocol.report_analog(pin.channel.unwrap(), true)?;
66
67        // Attaches the event handler.
68        sensor.attach();
69
70        Ok(sensor)
71    }
72
73    /// Returns the pin (id) used by the device.
74    pub fn get_pin(&self) -> u8 {
75        self.pin
76    }
77
78    // ########################################
79    // Event related functions
80
81    /// Manually attaches the AnalogInput with the value change events.
82    /// This should never be needed unless you manually `detach()` the AnalogInput first for some reason
83    /// and want it to start being reactive to events again.
84    pub fn attach(&self) {
85        if self.handler.read().is_none() {
86            let self_clone = self.clone();
87            *self.handler.write() = Some(
88                task::run(async move {
89                    loop {
90                        let pin_value = self_clone
91                            .protocol
92                            .get_io()
93                            .read()
94                            .get_pin(self_clone.pin)?
95                            .value;
96                        let state_value = *self_clone.state.read();
97                        if pin_value != state_value {
98                            *self_clone.state.write() = pin_value;
99                            self_clone.events.emit(InputEvent::OnChange, pin_value);
100                        }
101
102                        // Change can only be done 10x a sec. to avoid bouncing.
103                        pause!(100);
104                    }
105                    #[allow(unreachable_code)]
106                    Ok(())
107                })
108                .unwrap(),
109            );
110        }
111    }
112
113    /// Detaches the interval associated with the AnalogInput.
114    /// This means the AnalogInput won't react anymore to value changes.
115    pub fn detach(&self) {
116        if let Some(handler) = self.handler.read().as_ref() {
117            handler.abort();
118        }
119        *self.handler.write() = None
120    }
121
122    /// Registers a callback to be executed on a given event.
123    ///
124    /// Available events for an analog input are:
125    /// - **`InputEvent::OnChange` | `change`**: Triggered when the AnalogInput value changes.    
126    ///   _The callback must receive the following parameter: `|value: u16| { ... }`_
127    ///
128    /// # Example
129    ///
130    /// ```
131    /// use hermes_five::hardware::{Board, BoardEvent};
132    /// use hermes_five::devices::{AnalogInput, InputEvent};
133    ///
134    /// #[hermes_five::runtime]
135    /// async fn main() {
136    ///     let board = Board::run();
137    ///     board.on(BoardEvent::OnReady, |board: Board| async move {
138    ///
139    ///         // Register a Sensor on pin 14 (A0).
140    ///         let potentiometer = AnalogInput::new(&board, "A0")?;
141    ///         // Triggered function when the sensor state changes.
142    ///         potentiometer.on(InputEvent::OnChange, |value: u16| async move {
143    ///             println!("Sensor value changed: {}", value);
144    ///             Ok(())
145    ///         });
146    ///
147    ///         // The above code will run forever.
148    ///         // <do something useful>
149    ///
150    ///         // The above code will run forever runs a listener on the pin state under-the-hood.
151    ///         // It means the program will run forever listening to the InputEvent,
152    ///         // until we detach the device and close the board.
153    ///         potentiometer.detach();
154    ///         board.close();
155    ///
156    ///         Ok(())
157    ///     });
158    /// }
159    /// ```
160    pub fn on<S, F, T, Fut>(&self, event: S, callback: F) -> EventHandler
161    where
162        S: Into<String>,
163        T: 'static + Send + Sync + Clone,
164        F: FnMut(T) -> Fut + Send + 'static,
165        Fut: std::future::Future<Output = Result<(), Error>> + Send + 'static,
166    {
167        self.events.on(event, callback)
168    }
169}
170
171impl Display for AnalogInput {
172    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
173        write!(
174            f,
175            "AnalogInput (pin={}) [state={}]",
176            self.pin,
177            self.state.read(),
178        )
179    }
180}
181
182#[cfg_attr(feature = "serde", typetag::serde)]
183impl Device for AnalogInput {}
184
185#[cfg_attr(feature = "serde", typetag::serde)]
186impl Input for AnalogInput {
187    fn get_state(&self) -> State {
188        State::from(*self.state.read())
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use crate::devices::input::analog::AnalogInput;
195    use crate::devices::input::Input;
196    use crate::devices::input::InputEvent;
197    use crate::hardware::Board;
198    use crate::mocks::plugin_io::MockIoProtocol;
199    use crate::pause;
200    use std::sync::atomic::{AtomicU16, Ordering};
201    use std::sync::Arc;
202
203    #[hermes_five_macros::test]
204    fn test_new_analog_input() {
205        let board = Board::new(MockIoProtocol::default());
206        let sensor = AnalogInput::new(&board, 14);
207        assert!(sensor.is_ok());
208        let sensor = sensor.unwrap();
209        assert_eq!(sensor.get_pin(), 14);
210        assert_eq!(sensor.get_state().as_integer(), 100);
211        sensor.detach();
212
213        let sensor = AnalogInput::new(&board, "A22").unwrap();
214        assert_eq!(sensor.get_pin(), 22);
215        assert_eq!(sensor.get_state().as_integer(), 222);
216
217        sensor.detach();
218        board.close();
219    }
220
221    #[hermes_five_macros::test]
222    fn test_analog_display() {
223        let board = Board::new(MockIoProtocol::default());
224        let sensor = AnalogInput::new(&board, "A15").unwrap();
225        assert_eq!(sensor.get_state().as_integer(), 200);
226        assert_eq!(
227            format!("{}", sensor),
228            String::from("AnalogInput (pin=15) [state=200]")
229        );
230
231        sensor.detach();
232        board.close();
233    }
234
235    #[hermes_five_macros::test]
236    fn test_analog_events() {
237        let pin = "A14";
238        let board = Board::new(MockIoProtocol::default());
239        let sensor = AnalogInput::new(&board, pin).unwrap();
240        assert_eq!(sensor.get_state().as_integer(), 100);
241
242        // CHANGE
243        let change_flag = Arc::new(AtomicU16::new(100));
244        let moved_change_flag = change_flag.clone();
245        sensor.on(InputEvent::OnChange, move |new_state: u16| {
246            let captured_flag = moved_change_flag.clone();
247            async move {
248                captured_flag.store(new_state, Ordering::SeqCst);
249                Ok(())
250            }
251        });
252
253        assert_eq!(change_flag.load(Ordering::SeqCst), 100);
254
255        // Simulate pin state change in the protocol => take value 0xFF
256        sensor
257            .protocol
258            .get_io()
259            .write()
260            .get_pin_mut(pin)
261            .unwrap()
262            .value = 0xFF;
263
264        pause!(500);
265        assert_eq!(change_flag.load(Ordering::SeqCst), 0xFF);
266
267        sensor.detach();
268    }
269}