Skip to main content

status_led/
led.rs

1use crate::brightness::{self, Brightness};
2use embedded_hal::digital::{OutputPin, PinState, StatefulOutputPin};
3
4/// Runtime polarity mode.
5///
6/// Determines whether a logical ON maps to a physical `High` or `Low` pin level.
7/// Chosen at construction time, making it suitable for polarity derived from
8/// runtime configuration.
9///
10/// # Examples
11///
12/// ```ignore
13/// use status_led::{Led, PolarityMode};
14///
15/// let pol = if config.active_low {
16///     PolarityMode::ActiveLow
17/// } else {
18///     PolarityMode::ActiveHigh
19/// };
20/// let mut led = Led::new(pin, pol).unwrap();
21/// led.on().unwrap();
22/// ```
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24#[cfg_attr(feature = "defmt", derive(defmt::Format))]
25pub enum PolarityMode {
26    /// Active-high: pin High = LED on.
27    ActiveHigh,
28    /// Active-low: pin Low = LED on.
29    ActiveLow,
30}
31
32impl PolarityMode {
33    #[inline]
34    pub(crate) fn physical_on(self) -> PinState {
35        match self {
36            Self::ActiveHigh => PinState::High,
37            Self::ActiveLow => PinState::Low,
38        }
39    }
40
41    #[inline]
42    pub(crate) fn physical_off(self) -> PinState {
43        match self {
44            Self::ActiveHigh => PinState::Low,
45            Self::ActiveLow => PinState::High,
46        }
47    }
48
49    #[inline]
50    pub(crate) fn is_logical_on(self, physical_high: bool) -> bool {
51        match self {
52            Self::ActiveHigh => physical_high,
53            Self::ActiveLow => !physical_high,
54        }
55    }
56
57    #[inline]
58    #[allow(dead_code)] // only used when feature = "pwm"
59    pub(crate) fn map_duty(self, brightness: Brightness) -> Brightness {
60        match self {
61            Self::ActiveHigh => brightness,
62            // Wrapping subtraction: MAX - brightness inverts the duty for active-low.
63            Self::ActiveLow => brightness::max_value() - brightness,
64        }
65    }
66}
67
68// ─── Led ────────────────────────────────────────────
69
70/// Monochrome LED with polarity chosen at construction time.
71///
72/// Stores a [`PolarityMode`] value — each operation incurs one `match` which
73/// is negligible on Cortex-M.
74///
75/// No internal state cache — `is_on()` reads the hardware register (ODR) directly
76/// and applies the polarity conversion.  In the embassy ecosystem `OutputPin::Error`
77/// is [`Infallible`], so unwrapping results is safe.
78///
79/// # Examples
80///
81/// ```ignore
82/// use status_led::{Led, PolarityMode};
83///
84/// let mut led = Led::new(pin, PolarityMode::ActiveLow).unwrap();
85/// led.on().unwrap();
86/// led.toggle().unwrap();
87/// assert!(led.is_on().unwrap());
88/// ```
89///
90/// [`Infallible`]: core::convert::Infallible
91pub struct Led<P> {
92    pin: P,
93    polarity: PolarityMode,
94}
95
96impl<P> Led<P> {
97    /// Build from an already-configured pin without changing its level.
98    ///
99    /// Prefer this when the HAL already set the pin to a known state
100    /// (e.g. `Output::new(pin, Level::High, Speed::Low)` for an active-low LED).
101    #[inline]
102    pub fn from_pin(pin: P, polarity: PolarityMode) -> Self {
103        Self { pin, polarity }
104    }
105}
106
107impl<P: OutputPin> Led<P> {
108    /// Build and force the pin to the logical OFF state.
109    ///
110    /// Safest default — guarantees the LED starts dark regardless of the pin's
111    /// reset state.
112    pub fn new(mut pin: P, polarity: PolarityMode) -> Result<Self, P::Error> {
113        pin.set_state(polarity.physical_off())?;
114        Ok(Self { pin, polarity })
115    }
116
117    /// Turn the LED on (logical ON → physical level determined by polarity).
118    #[inline]
119    pub fn on(&mut self) -> Result<(), P::Error> {
120        self.pin.set_state(self.polarity.physical_on())
121    }
122
123    /// Turn the LED off (logical OFF → physical level determined by polarity).
124    #[inline]
125    pub fn off(&mut self) -> Result<(), P::Error> {
126        self.pin.set_state(self.polarity.physical_off())
127    }
128
129    /// Set the logical state: `true` = ON, `false` = OFF.
130    #[inline]
131    pub fn set(&mut self, state: bool) -> Result<(), P::Error> {
132        if state { self.on() } else { self.off() }
133    }
134}
135
136impl<P: StatefulOutputPin> Led<P> {
137    /// Read the logical state from the hardware register.
138    ///
139    /// Reads the physical pin level (ODR register on STM32), then converts
140    /// through the polarity.  Requires `&mut self` because
141    /// [`StatefulOutputPin::is_set_high`] does — in practice the read is a
142    /// single register access with no side effects.
143    #[inline]
144    pub fn is_on(&mut self) -> Result<bool, P::Error> {
145        self.pin
146            .is_set_high()
147            .map(|h| self.polarity.is_logical_on(h))
148    }
149
150    /// Inverse of [`is_on`](Self::is_on).
151    #[inline]
152    pub fn is_off(&mut self) -> Result<bool, P::Error> {
153        self.is_on().map(|on| !on)
154    }
155
156    /// Toggle the logical state.
157    ///
158    /// Delegates to [`StatefulOutputPin::toggle`].  On embassy this is a single
159    /// bit-band operation (`ODR ^= 1 << n`) — unconditionally flips the
160    /// physical pin, which is always correct regardless of polarity.
161    #[inline]
162    pub fn toggle(&mut self) -> Result<(), P::Error> {
163        self.pin.toggle()
164    }
165}
166
167impl<P> Led<P> {
168    /// Return the current polarity.
169    #[inline]
170    pub fn polarity(&self) -> PolarityMode {
171        self.polarity
172    }
173
174    /// Consume the `Led` and return the underlying pin.
175    #[inline]
176    pub fn release(self) -> P {
177        self.pin
178    }
179}
180
181impl<P> core::fmt::Debug for Led<P> {
182    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
183        f.debug_struct("Led")
184            .field("polarity", &self.polarity)
185            .finish_non_exhaustive()
186    }
187}
188
189#[cfg(feature = "defmt")]
190impl<P> defmt::Format for Led<P> {
191    fn format(&self, fmt: defmt::Formatter) {
192        defmt::write!(fmt, "Led {{ polarity: {} }}", self.polarity)
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199    use embedded_hal_mock::eh1::digital::{
200        Mock as PinMock, State as EState, Transaction as PinTrans,
201    };
202
203    #[test]
204    fn new_active_high_sets_off() {
205        let e = [PinTrans::set(EState::Low)];
206        Led::new(PinMock::new(&e), PolarityMode::ActiveHigh)
207            .unwrap()
208            .release()
209            .done();
210    }
211
212    #[test]
213    fn new_active_low_sets_off() {
214        let e = [PinTrans::set(EState::High)];
215        Led::new(PinMock::new(&e), PolarityMode::ActiveLow)
216            .unwrap()
217            .release()
218            .done();
219    }
220
221    #[test]
222    fn from_pin_no_touch() {
223        Led::from_pin(PinMock::new(&[]), PolarityMode::ActiveHigh)
224            .release()
225            .done();
226    }
227
228    #[test]
229    fn active_high_on_off() {
230        let e = [
231            PinTrans::set(EState::Low),
232            PinTrans::set(EState::High),
233            PinTrans::set(EState::Low),
234        ];
235        let mut led = Led::new(PinMock::new(&e), PolarityMode::ActiveHigh).unwrap();
236        led.on().unwrap();
237        led.off().unwrap();
238        led.release().done();
239    }
240
241    #[test]
242    fn active_low_on_off() {
243        let e = [
244            PinTrans::set(EState::High),
245            PinTrans::set(EState::Low),
246            PinTrans::set(EState::High),
247        ];
248        let mut led = Led::new(PinMock::new(&e), PolarityMode::ActiveLow).unwrap();
249        led.on().unwrap();
250        led.off().unwrap();
251        led.release().done();
252    }
253
254    #[test]
255    fn set_state() {
256        let e = [
257            PinTrans::set(EState::Low),
258            PinTrans::set(EState::High),
259            PinTrans::set(EState::Low),
260        ];
261        let mut led = Led::new(PinMock::new(&e), PolarityMode::ActiveHigh).unwrap();
262        led.set(true).unwrap();
263        led.set(false).unwrap();
264        led.release().done();
265    }
266
267    #[test]
268    fn toggle() {
269        let e = [PinTrans::set(EState::Low), PinTrans::toggle()];
270        let mut led = Led::new(PinMock::new(&e), PolarityMode::ActiveHigh).unwrap();
271        led.toggle().unwrap();
272        led.release().done();
273    }
274
275    #[test]
276    fn is_on_active_high() {
277        let e = [PinTrans::set(EState::Low), PinTrans::get_state(EState::Low)];
278        let mut led = Led::new(PinMock::new(&e), PolarityMode::ActiveHigh).unwrap();
279        assert!(!led.is_on().unwrap());
280        led.release().done();
281    }
282
283    #[test]
284    fn is_on_active_low() {
285        let e = [
286            PinTrans::set(EState::High),
287            PinTrans::get_state(EState::High),
288        ];
289        let mut led = Led::new(PinMock::new(&e), PolarityMode::ActiveLow).unwrap();
290        assert!(!led.is_on().unwrap());
291        led.release().done();
292    }
293
294    #[test]
295    fn polarity_accessor() {
296        let led = Led::from_pin(PinMock::new(&[]), PolarityMode::ActiveLow);
297        assert_eq!(led.polarity(), PolarityMode::ActiveLow);
298        led.release().done();
299    }
300}