hermes_five/devices/output/
digital.rs

1use std::fmt::{Display, Formatter};
2use std::sync::Arc;
3
4use parking_lot::RwLock;
5
6use crate::animations::{Animation, Easing, Keyframe, Track};
7use crate::devices::{Device, Output};
8use crate::errors::{Error, HardwareError, StateError};
9use crate::hardware::Hardware;
10use crate::io::{IoProtocol, Pin, PinIdOrName, PinModeId};
11use crate::utils::State;
12
13/// Represents a digital actuator of unspecified type: an [`Output`] [`Device`] that write digital values
14/// from an OUTPUT compatible pin.
15/// <https://docs.arduino.cc/language-reference/en/functions/digital-io/digitalwrite/>
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17#[derive(Clone, Debug)]
18pub struct DigitalOutput {
19    // ########################################
20    // # Basics
21    /// The pin (id) of the [`Board`] used to control the output value.
22    pin: u8,
23    /// The current output state.
24    #[cfg_attr(feature = "serde", serde(with = "crate::devices::arc_rwlock_serde"))]
25    state: Arc<RwLock<bool>>,
26    /// The output default value (default: 0).
27    default: bool,
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 animation.
34    #[cfg_attr(feature = "serde", serde(skip))]
35    animation: Arc<Option<Animation>>,
36}
37
38impl DigitalOutput {
39    /// Creates an instance of a [`DigitalOutput`] attached to a given board.
40    ///
41    /// # Errors
42    /// * `HardwareError::UnknownPin`: this function will bail an error if the pin does not exist for this board.
43    /// * `HardwareError::IncompatiblePin`: this function will bail an error if the pin does not support OUTPUT mode.
44    pub fn new<T: Into<PinIdOrName>>(
45        board: &dyn Hardware,
46        pin: T,
47        default: bool,
48    ) -> Result<Self, Error> {
49        let pin = board.get_io().read().get_pin(pin)?.clone();
50
51        let mut output = Self {
52            pin: pin.id,
53            state: Arc::new(RwLock::new(default)),
54            default,
55            protocol: board.get_protocol(),
56            animation: Arc::new(None),
57        };
58
59        // Set pin mode to OUTPUT.
60        output
61            .protocol
62            .set_pin_mode(output.pin, PinModeId::OUTPUT)?;
63
64        // Resets the output to default value.
65        output.reset()?;
66
67        Ok(output)
68    }
69
70    /// Turn the output HIGH.
71    pub fn turn_on(&mut self) -> Result<&Self, Error> {
72        self.set_state(State::Boolean(true))?;
73        Ok(self)
74    }
75
76    /// Turn the output LOW.
77    pub fn turn_off(&mut self) -> Result<&Self, Error> {
78        self.set_state(State::Boolean(false))?;
79        Ok(self)
80    }
81
82    /// Toggle the current state, if on then turn off, if off then turn on.
83    pub fn toggle(&mut self) -> Result<&Self, Error> {
84        match self.is_high() {
85            true => self.turn_off(),
86            false => self.turn_on(),
87        }
88    }
89
90    // ########################################
91    // Setters and Getters.
92
93    /// Returns the pin (id) used by the device.
94    pub fn get_pin(&self) -> u8 {
95        self.pin
96    }
97
98    /// Returns  [`Pin`] information.
99    pub fn get_pin_info(&self) -> Result<Pin, Error> {
100        let lock = self.protocol.get_io().read();
101        Ok(lock.get_pin(self.pin)?.clone())
102    }
103
104    /// Indicates if the device state is HIGH.
105    pub fn is_high(&self) -> bool {
106        *self.state.read()
107    }
108
109    /// Indicates if the device state is LOW.
110    pub fn is_low(&self) -> bool {
111        !*self.state.read()
112    }
113}
114
115impl Display for DigitalOutput {
116    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
117        write!(
118            f,
119            "DigitalOutput (pin={}) [state={}, default={}]",
120            self.pin,
121            self.state.read(),
122            self.default,
123        )
124    }
125}
126
127#[cfg_attr(feature = "serde", typetag::serde)]
128impl Device for DigitalOutput {}
129
130#[cfg_attr(feature = "serde", typetag::serde)]
131impl Output for DigitalOutput {
132    fn get_state(&self) -> State {
133        (*self.state.read()).into()
134    }
135
136    /// Internal only: you should rather use [`Self::turn_on()`], [`Self::turn_off()`] functions.
137    fn set_state(&mut self, state: State) -> Result<State, Error> {
138        let value = match state {
139            State::Boolean(value) => Ok(value),
140            State::Integer(value) => match value {
141                0 => Ok(false),
142                1 => Ok(true),
143                _ => Err(StateError),
144            },
145            _ => Err(StateError),
146        }?;
147
148        match self.get_pin_info()?.mode.id {
149            // on/off digital operation.
150            PinModeId::OUTPUT => self.protocol.digital_write(self.pin, value),
151            id => Err(Error::from(HardwareError::IncompatiblePin {
152                mode: id,
153                pin: self.pin,
154                context: "update digital output",
155            })),
156        }?;
157        *self.state.write() = value;
158        Ok(value.into())
159    }
160
161    fn get_default(&self) -> State {
162        self.default.into()
163    }
164
165    fn animate<S: Into<State>>(&mut self, state: S, duration: u64, transition: Easing) {
166        let mut animation = Animation::from(
167            Track::new(self.clone())
168                .with_keyframe(Keyframe::new(state, 0, duration).set_transition(transition)),
169        );
170        animation.play();
171        self.animation = Arc::new(Some(animation));
172    }
173
174    fn is_busy(&self) -> bool {
175        self.animation.is_some()
176    }
177
178    fn stop(&mut self) {
179        if let Some(animation) = Arc::get_mut(&mut self.animation).and_then(Option::as_mut) {
180            animation.stop();
181        }
182        self.animation = Arc::new(None);
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use crate::animations::Easing;
189    use crate::devices::output::digital::DigitalOutput;
190    use crate::devices::Output;
191    use crate::hardware::Board;
192    use crate::io::PinModeId;
193    use crate::mocks::plugin_io::MockIoProtocol;
194    use crate::pause;
195    use crate::utils::State;
196
197    #[test]
198    fn test_creation() {
199        let board = Board::new(MockIoProtocol::default());
200
201        // Default LOW state.
202        let output = DigitalOutput::new(&board, 13, false).unwrap();
203        assert_eq!(output.get_pin(), 13);
204        assert!(!*output.state.read());
205        assert!(!output.get_state().as_bool());
206        assert!(!output.get_default().as_bool());
207        assert!(output.is_low());
208        assert!(!output.is_high());
209
210        // Default HIGH state.
211        let output = DigitalOutput::new(&board, 4, true).unwrap();
212        assert_eq!(output.get_pin(), 4);
213        assert!(*output.state.read());
214        assert!(output.get_state().as_bool());
215        assert!(output.get_default().as_bool());
216        assert!(output.is_high());
217        assert!(!output.is_low());
218
219        // Created from pin name
220        let output = DigitalOutput::new(&board, "D13", true).unwrap();
221        assert_eq!(output.get_pin(), 13);
222
223        // Created for a ANALOG pin.
224        let output = DigitalOutput::new(&board, "A14", false).unwrap();
225        assert_eq!(output.get_pin(), 14);
226    }
227
228    #[test]
229    fn test_set_high() {
230        let mut output =
231            DigitalOutput::new(&Board::new(MockIoProtocol::default()), 4, false).unwrap();
232        output.turn_on().unwrap();
233        assert!(output.turn_on().is_ok());
234        assert!(*output.state.read());
235    }
236
237    #[test]
238    fn test_set_low() {
239        let mut output =
240            DigitalOutput::new(&Board::new(MockIoProtocol::default()), 5, true).unwrap();
241        assert!(output.turn_off().is_ok());
242        assert!(!*output.state.read());
243    }
244
245    #[test]
246    fn test_toggle() {
247        let mut output =
248            DigitalOutput::new(&Board::new(MockIoProtocol::default()), 5, false).unwrap();
249        assert!(output.toggle().is_ok()); // Toggle to HIGH
250        assert!(*output.state.read());
251        assert!(output.toggle().is_ok()); // Toggle to LOW
252        assert!(!*output.state.read());
253    }
254
255    #[test]
256    fn test_set_state() {
257        let mut output =
258            DigitalOutput::new(&Board::new(MockIoProtocol::default()), 13, false).unwrap();
259        assert!(output.set_state(State::Boolean(true)).is_ok());
260        assert!(*output.state.read());
261        assert!(output.set_state(State::Boolean(false)).is_ok());
262        assert!(!*output.state.read());
263
264        assert!(output.set_state(State::Integer(1)).is_ok());
265        assert!(*output.state.read());
266        assert!(output.set_state(State::Integer(0)).is_ok());
267        assert!(!*output.state.read());
268        assert!(output.set_state(State::Integer(42)).is_err());
269
270        assert!(output
271            .set_state(State::String(String::from("incorrect format")))
272            .is_err()); // Should return an error due to incompatible state
273                        // Force an incompatible pin mode
274        let _ = output
275            .protocol
276            .set_pin_mode(output.pin, PinModeId::UNSUPPORTED);
277        assert!(output.set_state(State::Boolean(false)).is_err()); // Should return an error due to incompatible pin mode.
278    }
279
280    #[test]
281    fn test_get_pin_info() {
282        let output = DigitalOutput::new(&Board::new(MockIoProtocol::default()), 13, false).unwrap();
283        let pin_info = output.get_pin_info();
284        assert!(pin_info.is_ok());
285        assert_eq!(pin_info.unwrap().id, 13);
286    }
287
288    #[hermes_five_macros::test]
289    fn test_animation() {
290        let mut output =
291            DigitalOutput::new(&Board::new(MockIoProtocol::default()), 13, false).unwrap();
292        assert!(!output.is_busy());
293        // Stop something not started should not fail.
294        output.stop();
295        // This animation does not make sense !
296        output.animate(true, 500, Easing::Linear);
297        pause!(100);
298        assert!(output.is_busy()); // Animation is currently running.
299        output.stop();
300    }
301
302    #[test]
303    fn test_display_impl() {
304        let mut output =
305            DigitalOutput::new(&Board::new(MockIoProtocol::default()), 13, true).unwrap();
306        let _ = output.turn_off();
307        let display_str = format!("{}", output);
308        assert_eq!(
309            display_str,
310            "DigitalOutput (pin=13) [state=false, default=true]"
311        );
312    }
313}