Skip to main content

status_led/
led.rs

1use core::marker::PhantomData;
2use embedded_hal::digital::{OutputPin, StatefulOutputPin};
3
4use crate::polarity::{Polarity, PolarityMode};
5
6/// Monochrome LED with compile-time polarity.
7///
8/// Type parameters:
9/// - `P`: pin type implementing [`OutputPin`] and [`StatefulOutputPin`].
10/// - `POL`: polarity marker, [`ActiveHigh`] or [`ActiveLow`] (default [`ActiveHigh`]).
11///
12/// No internal state cache — `is_on()` reads the hardware register (ODR) directly
13/// and applies the polarity conversion.  In the embassy ecosystem `OutputPin::Error`
14/// is [`Infallible`], so unwrapping results is safe.
15///
16/// # Examples
17///
18/// ```ignore
19/// use status_led::{Led, ActiveLow};
20///
21/// let mut led = Led::<_, ActiveLow>::new(pin).unwrap();
22/// led.on().unwrap();
23/// led.toggle().unwrap();
24/// assert!(led.is_on().unwrap());
25/// ```
26///
27/// [`ActiveHigh`]: crate::ActiveHigh
28/// [`ActiveLow`]: crate::ActiveLow
29/// [`Infallible`]: core::convert::Infallible
30pub struct Led<P, POL = crate::ActiveHigh> {
31    pin: P,
32    _polarity: PhantomData<POL>,
33}
34
35impl<P, POL> Led<P, POL> {
36    /// Build from an already-configured pin without changing its level.
37    ///
38    /// Prefer this when the HAL already set the pin to a known state
39    /// (e.g. `Output::new(pin, Level::High, Speed::Low)` for an active-low LED).
40    #[inline]
41    pub fn from_pin(pin: P) -> Self {
42        Self {
43            pin,
44            _polarity: PhantomData,
45        }
46    }
47}
48
49impl<P: OutputPin, POL: Polarity> Led<P, POL> {
50    /// Build and force the pin to the logical OFF state.
51    ///
52    /// Safest default — guarantees the LED starts dark regardless of the pin's
53    /// reset state.
54    pub fn new(mut pin: P) -> Result<Self, P::Error> {
55        pin.set_state(POL::physical_off())?;
56        Ok(Self {
57            pin,
58            _polarity: PhantomData,
59        })
60    }
61
62    /// Turn the LED on (logical ON → physical level determined by polarity).
63    #[inline]
64    pub fn on(&mut self) -> Result<(), P::Error> {
65        self.pin.set_state(POL::physical_on())
66    }
67
68    /// Turn the LED off (logical OFF → physical level determined by polarity).
69    #[inline]
70    pub fn off(&mut self) -> Result<(), P::Error> {
71        self.pin.set_state(POL::physical_off())
72    }
73
74    /// Set the logical state: `true` = ON, `false` = OFF.
75    #[inline]
76    pub fn set(&mut self, state: bool) -> Result<(), P::Error> {
77        if state { self.on() } else { self.off() }
78    }
79}
80
81impl<P: StatefulOutputPin, POL: Polarity> Led<P, POL> {
82    /// Read the logical state from the hardware register.
83    ///
84    /// Reads the physical pin level (ODR register on STM32), then converts
85    /// through the polarity.  Requires `&mut self` because
86    /// [`StatefulOutputPin::is_set_high`] does — in practice the read is a
87    /// single register access with no side effects.
88    #[inline]
89    pub fn is_on(&mut self) -> Result<bool, P::Error> {
90        self.pin.is_set_high().map(|h| POL::is_logical_on(h))
91    }
92
93    /// Inverse of [`is_on`](Self::is_on).
94    #[inline]
95    pub fn is_off(&mut self) -> Result<bool, P::Error> {
96        self.is_on().map(|on| !on)
97    }
98
99    /// Toggle the logical state.
100    ///
101    /// Delegates to [`StatefulOutputPin::toggle`].  On embassy this is a single
102    /// bit-band operation (`ODR ^= 1 << n`) — unconditionally flips the
103    /// physical pin, which is always correct regardless of polarity.
104    #[inline]
105    pub fn toggle(&mut self) -> Result<(), P::Error> {
106        self.pin.toggle()
107    }
108}
109
110impl<P, POL: Polarity> Led<P, POL> {
111    /// Consume the `Led` and return the underlying pin.
112    #[inline]
113    pub fn release(self) -> P {
114        self.pin
115    }
116
117    /// Convert to a [`FlexLed`], preserving the current polarity.
118    pub fn into_flex(self) -> FlexLed<P> {
119        FlexLed {
120            pin: self.pin,
121            polarity: POL::MODE,
122        }
123    }
124}
125
126impl<P, POL: Polarity> core::fmt::Debug for Led<P, POL> {
127    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
128        f.debug_struct("Led")
129            .field("polarity", &core::any::type_name::<POL>())
130            .finish_non_exhaustive()
131    }
132}
133
134#[cfg(feature = "defmt")]
135impl<P, POL: Polarity> defmt::Format for Led<P, POL> {
136    fn format(&self, fmt: defmt::Formatter) {
137        defmt::write!(
138            fmt,
139            "Led {{ polarity: {} }}",
140            defmt::Display2Format(core::any::type_name::<POL>())
141        )
142    }
143}
144
145// ─── FlexLed ─────────────────────────────────────────
146
147/// LED with runtime-determined polarity.
148///
149/// Unlike [`Led`] which bakes polarity into the type, `FlexLed` stores it as a
150/// [`PolarityMode`] value.  Useful when the polarity is read from configuration
151/// at runtime rather than known at compile time.  Each operation incurs one
152/// extra `match` vs `Led` — negligible on Cortex-M.
153///
154/// # Examples
155///
156/// ```ignore
157/// use status_led::{FlexLed, PolarityMode};
158///
159/// let pol = if config.active_low {
160///     PolarityMode::ActiveLow
161/// } else {
162///     PolarityMode::ActiveHigh
163/// };
164/// let mut led = FlexLed::new(pin, pol).unwrap();
165/// led.on().unwrap();
166/// ```
167pub struct FlexLed<P> {
168    pin: P,
169    polarity: PolarityMode,
170}
171
172impl<P> FlexLed<P> {
173    /// Build from an already-configured pin without changing its level.
174    #[inline]
175    pub fn from_pin(pin: P, polarity: PolarityMode) -> Self {
176        Self { pin, polarity }
177    }
178}
179
180impl<P: OutputPin> FlexLed<P> {
181    /// Build and force the pin to the logical OFF state.
182    pub fn new(mut pin: P, polarity: PolarityMode) -> Result<Self, P::Error> {
183        pin.set_state(polarity.physical_off())?;
184        Ok(Self { pin, polarity })
185    }
186
187    /// Turn the LED on.
188    #[inline]
189    pub fn on(&mut self) -> Result<(), P::Error> {
190        self.pin.set_state(self.polarity.physical_on())
191    }
192
193    /// Turn the LED off.
194    #[inline]
195    pub fn off(&mut self) -> Result<(), P::Error> {
196        self.pin.set_state(self.polarity.physical_off())
197    }
198
199    /// Set the logical state: `true` = ON, `false` = OFF.
200    #[inline]
201    pub fn set(&mut self, state: bool) -> Result<(), P::Error> {
202        if state { self.on() } else { self.off() }
203    }
204}
205
206impl<P: StatefulOutputPin> FlexLed<P> {
207    /// Read the logical state from the hardware register.
208    #[inline]
209    pub fn is_on(&mut self) -> Result<bool, P::Error> {
210        self.pin
211            .is_set_high()
212            .map(|h| self.polarity.is_logical_on(h))
213    }
214
215    /// Inverse of [`is_on`](Self::is_on).
216    #[inline]
217    pub fn is_off(&mut self) -> Result<bool, P::Error> {
218        self.is_on().map(|on| !on)
219    }
220
221    /// Toggle the logical state.
222    #[inline]
223    pub fn toggle(&mut self) -> Result<(), P::Error> {
224        self.pin.toggle()
225    }
226}
227
228impl<P> FlexLed<P> {
229    /// Return the current polarity.
230    #[inline]
231    pub fn polarity(&self) -> PolarityMode {
232        self.polarity
233    }
234
235    /// Consume the `FlexLed` and return the underlying pin.
236    #[inline]
237    pub fn release(self) -> P {
238        self.pin
239    }
240}
241
242impl<P, POL: Polarity> From<Led<P, POL>> for FlexLed<P> {
243    fn from(led: Led<P, POL>) -> Self {
244        led.into_flex()
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251    use embedded_hal_mock::eh1::digital::{
252        Mock as PinMock, State as EState, Transaction as PinTrans,
253    };
254
255    #[test]
256    fn new_active_high_sets_off() {
257        let e = [PinTrans::set(EState::Low)];
258        Led::<_, crate::ActiveHigh>::new(PinMock::new(&e))
259            .unwrap()
260            .release()
261            .done();
262    }
263
264    #[test]
265    fn new_active_low_sets_off() {
266        let e = [PinTrans::set(EState::High)];
267        Led::<_, crate::ActiveLow>::new(PinMock::new(&e))
268            .unwrap()
269            .release()
270            .done();
271    }
272
273    #[test]
274    fn from_pin_no_touch() {
275        Led::<_, crate::ActiveHigh>::from_pin(PinMock::new(&[]))
276            .release()
277            .done();
278    }
279
280    #[test]
281    fn active_high_on_off() {
282        let e = [
283            PinTrans::set(EState::Low),
284            PinTrans::set(EState::High),
285            PinTrans::set(EState::Low),
286        ];
287        let mut led = Led::<_, crate::ActiveHigh>::new(PinMock::new(&e)).unwrap();
288        led.on().unwrap();
289        led.off().unwrap();
290        led.release().done();
291    }
292
293    #[test]
294    fn active_low_on_off() {
295        let e = [
296            PinTrans::set(EState::High),
297            PinTrans::set(EState::Low),
298            PinTrans::set(EState::High),
299        ];
300        let mut led = Led::<_, crate::ActiveLow>::new(PinMock::new(&e)).unwrap();
301        led.on().unwrap();
302        led.off().unwrap();
303        led.release().done();
304    }
305
306    #[test]
307    fn set_state() {
308        let e = [
309            PinTrans::set(EState::Low),
310            PinTrans::set(EState::High),
311            PinTrans::set(EState::Low),
312        ];
313        let mut led = Led::<_, crate::ActiveHigh>::new(PinMock::new(&e)).unwrap();
314        led.set(true).unwrap();
315        led.set(false).unwrap();
316        led.release().done();
317    }
318
319    #[test]
320    fn toggle() {
321        let e = [PinTrans::set(EState::Low), PinTrans::toggle()];
322        let mut led = Led::<_, crate::ActiveHigh>::new(PinMock::new(&e)).unwrap();
323        led.toggle().unwrap();
324        led.release().done();
325    }
326
327    #[test]
328    fn is_on_active_high() {
329        let e = [PinTrans::set(EState::Low), PinTrans::get_state(EState::Low)];
330        let mut led = Led::<_, crate::ActiveHigh>::new(PinMock::new(&e)).unwrap();
331        assert!(!led.is_on().unwrap());
332        led.release().done();
333    }
334
335    #[test]
336    fn is_on_active_low() {
337        let e = [
338            PinTrans::set(EState::High),
339            PinTrans::get_state(EState::High),
340        ];
341        let mut led = Led::<_, crate::ActiveLow>::new(PinMock::new(&e)).unwrap();
342        assert!(!led.is_on().unwrap());
343        led.release().done();
344    }
345
346    #[test]
347    fn flex_new_active_low() {
348        let e = [PinTrans::set(EState::High), PinTrans::set(EState::Low)];
349        let mut led = FlexLed::new(PinMock::new(&e), PolarityMode::ActiveLow).unwrap();
350        led.on().unwrap();
351        led.release().done();
352    }
353
354    #[test]
355    fn flex_from_pin_no_touch() {
356        let led = FlexLed::from_pin(PinMock::new(&[]), PolarityMode::ActiveLow);
357        assert_eq!(led.polarity(), PolarityMode::ActiveLow);
358        led.release().done();
359    }
360
361    #[test]
362    fn flex_toggle() {
363        let e = [PinTrans::set(EState::Low), PinTrans::toggle()];
364        let mut led = FlexLed::new(PinMock::new(&e), PolarityMode::ActiveHigh).unwrap();
365        led.toggle().unwrap();
366        led.release().done();
367    }
368
369    #[test]
370    fn flex_is_on() {
371        let e = [PinTrans::set(EState::Low), PinTrans::get_state(EState::Low)];
372        let mut led = FlexLed::new(PinMock::new(&e), PolarityMode::ActiveHigh).unwrap();
373        assert!(!led.is_on().unwrap());
374        led.release().done();
375    }
376
377    #[test]
378    fn into_flex_preserves_polarity() {
379        let led = Led::<_, crate::ActiveLow>::from_pin(PinMock::new(&[]));
380        let flex: FlexLed<_> = led.into_flex();
381        assert_eq!(flex.polarity(), PolarityMode::ActiveLow);
382        flex.release().done();
383    }
384
385    #[test]
386    fn from_led_to_flex() {
387        let led = Led::<_, crate::ActiveLow>::from_pin(PinMock::new(&[]));
388        let flex = FlexLed::from(led);
389        assert_eq!(flex.polarity(), PolarityMode::ActiveLow);
390        flex.release().done();
391    }
392}