Skip to main content

status_led/
led.rs

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