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