hermes_five/devices/output/
led.rs

1use std::fmt::{Display, Formatter};
2use std::sync::Arc;
3
4use parking_lot::RwLock;
5
6use crate::animations::{Animation, Easing, Keyframe, Segment, Track};
7use crate::devices::{Device, Output};
8use crate::errors::HardwareError::IncompatiblePin;
9use crate::errors::{Error, StateError};
10use crate::hardware::Hardware;
11use crate::io::{IoProtocol, Pin, PinMode, PinModeId};
12use crate::utils::{Scalable, State};
13
14/// Represents a LED controlled by a digital pin.
15/// There are two kinds of pins that can be used:
16/// - OUTPUT: for digital on/off led
17/// - PWM: for more control on the LED brightness
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19#[derive(Clone, Debug)]
20pub struct Led {
21    // ########################################
22    // # Basics
23    /// The pin (id) of the [`Board`] used to control the LED.
24    pin: u8,
25    /// The current LED state.
26    #[cfg_attr(feature = "serde", serde(with = "crate::devices::arc_rwlock_serde"))]
27    state: Arc<RwLock<u16>>,
28    /// The LED default value (default: 0 - OFF).
29    default: u16,
30
31    // ########################################
32    // # Settings
33    /// Indicates the current LED brightness when ON.
34    brightness: u16,
35
36    // ########################################
37    // # Volatile utility data.
38    /// If the pin can do PWM, we store that mode here (memoization use only).
39    #[cfg_attr(feature = "serde", serde(skip))]
40    pwm_mode: Option<PinMode>,
41    #[cfg_attr(feature = "serde", serde(skip))]
42    protocol: Box<dyn IoProtocol>,
43    /// Inner handler to the task running the animation.
44    #[cfg_attr(feature = "serde", serde(skip))]
45    animation: Arc<Option<Animation>>,
46}
47
48impl Led {
49    /// Creates an instance of a LED attached to a given board.
50    ///
51    /// # Errors
52    /// * `UnknownPin`: this function will bail an error if the pin does not exist for this board.
53    /// * `IncompatibleMode`: this function will bail an error if the pin does not support OUTPUT or PWM mode.
54    pub fn new(board: &dyn Hardware, pin: u8, default: bool) -> Result<Self, Error> {
55        let mut protocol = board.get_protocol();
56
57        // Get the hardware corresponding pin.
58        let hardware_pin = {
59            let hardware = protocol.get_io().read();
60            hardware.get_pin(pin)?.clone()
61        };
62
63        // Get the PWM mode if any
64        let pwm_mode = hardware_pin.supports_mode(PinModeId::PWM);
65
66        // Set pin mode to OUTPUT/PWM and compute default value accordingly.
67        let pin_mode = match pwm_mode {
68            None => PinModeId::OUTPUT,
69            Some(_) => PinModeId::PWM,
70        };
71        protocol.set_pin_mode(pin, pin_mode)?;
72
73        // Compute default value accordingly: 0 or 255 (max brightness).
74        let default = match default {
75            false => 0,
76            true => 0xFF,
77        };
78
79        let mut led = Self {
80            pin,
81            state: Arc::new(RwLock::new(default)),
82            default,
83            brightness: 0xFF,
84            pwm_mode,
85            protocol,
86            animation: Arc::new(None),
87        };
88
89        led.reset()?;
90
91        Ok(led)
92    }
93
94    /// Turns the LED on.
95    pub fn turn_on(&mut self) -> Result<&Self, Error> {
96        self.set_state(State::Integer(self.brightness as u64))?;
97        Ok(self)
98    }
99
100    /// Turns the LED off.
101    pub fn turn_off(&mut self) -> Result<&Self, Error> {
102        self.set_state(State::Integer(0))?;
103        Ok(self)
104    }
105
106    /// Toggles the current state, if on then turn off, if off then turn on.
107    pub fn toggle(&mut self) -> Result<&Self, Error> {
108        match self.is_on() {
109            true => self.turn_off(),
110            false => self.turn_on(),
111        }
112    }
113
114    /// Blinks the LED on/off in phases of milliseconds duration.
115    /// This is an animation and can be stopped by calling [`Led::stop()`].
116    pub fn blink(&mut self, ms: u64) -> &Self {
117        let mut animation = Animation::from(
118            Segment::from(
119                Track::new(self.clone())
120                    .with_keyframe(Keyframe::new(true, 0, ms))
121                    .with_keyframe(Keyframe::new(false, ms, ms * 2)),
122            )
123            .set_repeat(true),
124        );
125        animation.play();
126        self.animation = Arc::new(Some(animation));
127
128        self
129    }
130
131    /// Pulses the LED on/off (using fading) in phases of ms (milliseconds) duration.
132    /// This is an animation and can be stopped by calling [`Led::stop()`].
133    pub fn pulse(&mut self, ms: u64) -> &Self {
134        let mut animation = Animation::from(
135            Segment::from(
136                Track::new(self.clone())
137                    .with_keyframe(Keyframe::new(0xFFu16, 0, ms))
138                    .with_keyframe(Keyframe::new(0u16, ms, ms * 2)),
139            )
140            .set_repeat(true),
141        );
142        animation.play();
143        self.animation = Arc::new(Some(animation));
144
145        self
146    }
147
148    // ########################################
149    // Getters.
150
151    /// Returns the pin (id) used by the device.
152    pub fn get_pin(&self) -> u8 {
153        self.pin
154    }
155
156    /// Returns the [`Pin`] information.
157    pub fn get_pin_info(&self) -> Result<Pin, Error> {
158        let lock = self.protocol.get_io().read();
159        Ok(lock.get_pin(self.pin)?.clone())
160    }
161
162    /// Returns the LED current brightness in percentage (0-100%).
163    pub fn get_brightness(&self) -> u8 {
164        match self.pwm_mode {
165            None => 100,
166            // Compute the brightness percentage (depending on resolution (255 on arduino for instance)).
167            Some(pwm_mode) => self
168                .brightness
169                .scale(0, pwm_mode.get_max_possible_value(), 0, 100),
170        }
171    }
172
173    /// Set the LED brightness (integer between 0-100) in percent of the max brightness. If a number
174    /// higher than 100 is used, the brightness is set to 100%.
175    /// If the requested brightness is 100%, the LED will reset to simple on/off (OUTPUT) mode.
176    ///
177    /// # Errors
178    /// * `IncompatiblePin`: this function will bail an error if the LED pin does not support PWM.
179    pub fn set_brightness(mut self, brightness: u8) -> Result<Self, Error> {
180        // Brightness can only be between 0 and 100%
181        let brightness = brightness.clamp(0, 100) as u16;
182
183        // If the LED can use pwm mode: update the brightness
184        let pwm_mode = self.pwm_mode.ok_or(IncompatiblePin {
185            mode: PinModeId::PWM,
186            pin: self.pin,
187            context: "set LED brightness",
188        })?;
189
190        // Compute the brightness value (depending on resolution (255 on arduino for instance))
191        let brightness = brightness.scale(0, 100, 0, pwm_mode.get_max_possible_value());
192
193        // Sets the brightness.
194        self.brightness = brightness;
195
196        // If the value is higher than the brightness, we update it on the spot.
197        if self.state.read().ne(&self.brightness) {
198            self.set_state(State::Integer(self.brightness as u64))?;
199        }
200
201        Ok(self)
202    }
203
204    /// Indicates if the LED is current ON (regardless its brightness).
205    pub fn is_on(&self) -> bool {
206        self.state.read().gt(&0)
207    }
208
209    /// Indicates if the LED is current OFF.
210    pub fn is_off(&self) -> bool {
211        self.state.read().eq(&0)
212    }
213}
214
215impl Display for Led {
216    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
217        write!(
218            f,
219            "LED (pin={}) [state={}, default={}, brightness={}]",
220            self.pin,
221            self.state.read(),
222            self.default,
223            self.brightness
224        )
225    }
226}
227
228#[cfg_attr(feature = "serde", typetag::serde)]
229impl Device for Led {}
230
231#[cfg_attr(feature = "serde", typetag::serde)]
232impl Output for Led {
233    /// Returns  the actuator current state.
234    fn get_state(&self) -> State {
235        (*self.state.read()).into()
236    }
237
238    /// Internal only: you should rather use [`Self::turn_on()`], [`Self::turn_off()`], [`Self::set_brightness()`] functions.
239    fn set_state(&mut self, state: State) -> Result<State, Error> {
240        let value = match state {
241            State::Boolean(value) => match value {
242                true => Ok(self.brightness),
243                false => Ok(0),
244            },
245            State::Integer(value) => Ok(value as u16),
246            State::Float(value) => Ok(value as u16),
247            State::Signed(value) => Ok(value.max(0) as u16),
248            _ => Err(StateError),
249        }?;
250
251        match self.get_pin_info()?.mode.id {
252            // on/off digital operation.
253            PinModeId::OUTPUT => self.protocol.digital_write(self.pin, value > 0),
254            // pwm (brightness) mode.
255            PinModeId::PWM => self.protocol.analog_write(self.pin, value),
256            id => Err(Error::from(IncompatiblePin {
257                mode: id,
258                pin: self.pin,
259                context: "update LED",
260            })),
261        }?;
262        *self.state.write() = value;
263        Ok(value.into())
264    }
265    fn get_default(&self) -> State {
266        self.default.into()
267    }
268    fn animate<S: Into<State>>(&mut self, state: S, duration: u64, transition: Easing) {
269        let mut animation = Animation::from(
270            Track::new(self.clone())
271                .with_keyframe(Keyframe::new(state, 0, duration).set_transition(transition)),
272        );
273        animation.play();
274        self.animation = Arc::new(Some(animation));
275    }
276    fn is_busy(&self) -> bool {
277        self.animation.is_some()
278    }
279    fn stop(&mut self) {
280        if let Some(animation) = Arc::get_mut(&mut self.animation).and_then(Option::as_mut) {
281            animation.stop();
282        }
283        self.animation = Arc::new(None);
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use crate::hardware::Board;
290    use crate::mocks::plugin_io::MockIoProtocol;
291    use crate::pause;
292
293    use super::*;
294
295    fn _setup_led(pin: u8) -> Led {
296        let board = Board::new(MockIoProtocol::default()); // Assuming a mock Board implementation
297        Led::new(&board, pin, false).unwrap()
298    }
299
300    #[test]
301    fn test_led_creation() {
302        let led = _setup_led(13);
303        assert_eq!(led.get_pin(), 13); // Ensure the correct pin is set
304        assert_eq!(*led.state.read(), 0); // Initial state should be 0 (OFF)
305        assert_eq!(led.brightness, 0xFF); // Default brightness should be 255
306    }
307
308    #[test]
309    fn test_turn_on() {
310        let mut led = _setup_led(13);
311        assert!(led.turn_on().is_ok()); // Turn LED on
312        assert_eq!(*led.state.read(), 0xFF); // State should reflect the brightness (255)
313    }
314
315    #[test]
316    fn test_turn_off() {
317        let mut led = _setup_led(13);
318        led.turn_on().unwrap(); // Turn LED on first
319        assert!(led.turn_off().is_ok()); // Turn LED off
320        assert_eq!(*led.state.read(), 0); // State should be 0 (OFF)
321    }
322
323    #[test]
324    fn test_toggle() {
325        let mut led = _setup_led(13);
326        assert!(led.toggle().is_ok()); // Toggle to ON
327        assert_eq!(*led.state.read(), 0xFF); // Should be ON (255)
328        assert!(led.toggle().is_ok()); // Toggle to OFF
329        assert_eq!(*led.state.read(), 0); // Should be OFF (0)
330    }
331
332    #[test]
333    fn test_set_state() {
334        let mut led = _setup_led(13);
335
336        assert!(led.set_state(State::Boolean(true)).is_ok());
337        assert_eq!(*led.state.read(), 0xFF); // State should reflect the brightness (100% = 255)
338        assert!(led.set_state(State::Boolean(false)).is_ok());
339        assert_eq!(*led.state.read(), 0x00); // Should be OFF (0)
340
341        assert!(led.set_state(State::Integer(50)).is_ok());
342        assert_eq!(*led.state.read(), 50);
343        assert!(led.set_state(State::Float(60.0)).is_ok());
344        assert_eq!(*led.state.read(), 60);
345        assert!(led.set_state(State::Signed(70)).is_ok());
346        assert_eq!(*led.state.read(), 70);
347        assert!(led.set_state(State::Signed(-70)).is_ok());
348        assert_eq!(*led.state.read(), 0);
349
350        // Incorrect state type.
351        assert!(led
352            .set_state(State::String(String::from("incorrect format")))
353            .is_err()); // Should return an error due to incompatible state
354                        // Force an incompatible pin mode
355
356        // Incorrect pin type.
357        let _ = led.protocol.set_pin_mode(led.pin, PinModeId::UNSUPPORTED);
358        assert!(led.set_state(State::Boolean(false)).is_err()); // Should return an error due to incompatible pin mode.
359    }
360
361    #[test]
362    fn test_brightness_calculation() {
363        let mut led = _setup_led(8);
364
365        // Force custom pinMode on 10bits
366        led.pwm_mode = Some(PinMode {
367            id: Default::default(),
368            resolution: 10,
369        });
370
371        // Check brightness at 0%
372        let led = led.set_brightness(0).unwrap();
373        assert_eq!(led.get_brightness(), 0);
374        assert_eq!(led.brightness, 0);
375        assert_eq!(*led.state.read(), 0);
376
377        // Check brightness at 50%
378        let led = led.set_brightness(50).unwrap();
379        assert_eq!(led.get_brightness(), 50);
380        assert_eq!(led.brightness, 512);
381        assert_eq!(*led.state.read(), 512);
382
383        // Check brightness at 100%
384        let led = led.set_brightness(100).unwrap();
385        assert_eq!(led.get_brightness(), 100);
386        assert_eq!(led.brightness, 1023);
387        assert_eq!(*led.state.read(), 1023);
388
389        // Check brightness at 120%
390        let led = led.set_brightness(120).unwrap();
391        assert_eq!(led.get_brightness(), 100);
392        assert_eq!(led.brightness, 1023);
393        assert_eq!(*led.state.read(), 1023);
394    }
395
396    #[test]
397    fn test_set_brightness_valid() {
398        let result = _setup_led(8).set_brightness(50);
399        assert!(result.is_ok()); // Set brightness to 50%
400        let mut led = result.unwrap();
401
402        assert_eq!(led.get_brightness(), 50); // Check the brightness is correctly set
403        assert_eq!(led.brightness, 128); // 50% of 255
404        assert_eq!(*led.state.read(), 128); // State should reflect the brightness (50%)
405
406        assert_eq!(led.get_brightness(), 50); // Check the brightness is correctly set
407        assert_eq!(led.brightness, 128); // 50% of 255
408        assert_eq!(*led.state.read(), 128); // State should reflect the brightness (50%)
409
410        assert!(led.set_state(State::Boolean(false)).is_ok());
411        assert_eq!(*led.state.read(), 0x00);
412        assert!(led.set_state(State::Boolean(true)).is_ok());
413        assert_eq!(*led.state.read(), 128); // State should reflect the brightness (50%)
414    }
415
416    #[test]
417    fn test_set_brightness_incompatible_mode() {
418        let led = _setup_led(13);
419        assert_eq!(led.get_brightness(), 100);
420        let result = led.set_brightness(50);
421        assert!(result.is_err()); // Should return an error due to incompatible mode
422    }
423
424    #[test]
425    fn test_default_value() {
426        let led = _setup_led(13);
427        assert_eq!(led.get_state().as_integer(), 0); // Should be full OFF by default.
428        let led = Led::new(&Board::new(MockIoProtocol::default()), 13, true).unwrap(); // Setup with default value TRUE
429        assert_eq!(led.get_default().as_integer(), 0xFF); // Default should be fully ON (255).
430        assert_eq!(led.get_state().as_integer(), 0xFF); // State should be equal to default.
431    }
432
433    #[test]
434    fn test_get_pin_info() {
435        let led = _setup_led(13);
436        let pin_info = led.get_pin_info();
437        assert!(pin_info.is_ok()); // Ensure that pin information retrieval is successful
438    }
439
440    #[hermes_five_macros::test]
441    fn test_led_blink() {
442        let mut led = _setup_led(13);
443        assert!(!led.is_busy());
444        led.stop(); // Stop something not started should not fail.
445        led.blink(50); // Set a blink interval of 50 ms
446        pause!(100);
447        assert!(led.is_busy()); // Animation is currently running.
448        led.stop();
449        assert!(!led.is_busy());
450    }
451
452    #[hermes_five_macros::test]
453    fn test_led_pulse() {
454        let mut led = _setup_led(8);
455        assert!(!led.is_busy());
456        led.stop(); // Stop something not started should not fail.
457        led.pulse(50); // Set a fading pulse interval of 50 ms
458        pause!(100);
459        assert!(led.is_busy()); // Animation is currently running.
460        led.stop();
461        assert!(!led.is_busy());
462    }
463
464    #[hermes_five_macros::test]
465    fn test_animation() {
466        let mut led = _setup_led(8);
467        assert!(!led.is_busy());
468        // Stop something not started should not fail.
469        led.stop();
470        // Fade in the LED to brightness
471        led.animate(led.get_brightness(), 500, Easing::Linear);
472        pause!(100);
473        assert!(led.is_busy()); // Animation is currently running.
474        led.stop();
475    }
476
477    #[test]
478    fn test_is_on_off() {
479        let mut led = _setup_led(13);
480        assert!(!led.is_on()); // Initially the LED is off
481        assert!(led.is_off());
482        led.turn_on().unwrap();
483        assert!(led.is_on()); // After turning on, the LED should be on
484        assert!(!led.is_off());
485    }
486
487    #[test]
488    fn test_display_impl() {
489        let led = _setup_led(13);
490        let display_str = format!("{}", led);
491        assert!(display_str.contains("LED (pin=13) [state=0, default=0, brightness=255]"));
492    }
493}