Skip to main content

device_envoy/
led.rs

1//! A device abstraction for a single digital LED with animation support.
2//!
3//! This module provides a simple interface for controlling a single GPIO-connected LED
4//! with support for on/off control and animated blinking sequences.
5//!
6//! See [`Led`] for the primary example and usage.
7
8use core::borrow::Borrow;
9use embassy_executor::Spawner;
10use embassy_rp::Peri;
11use embassy_rp::gpio::{Level, Output};
12use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal};
13use embassy_time::{Duration, Timer};
14use heapless::Vec;
15
16use crate::{Error, Result};
17
18// ============================================================================
19// Constants
20// ============================================================================
21
22/// Maximum number of animation frames allowed.
23const MAX_FRAMES: usize = 32;
24
25// ============================================================================
26// OnLevel - What pin level turns the LED on
27// ============================================================================
28
29/// What pin level turns the LED on (depends on wiring).
30#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, defmt::Format, Default)]
31pub enum OnLevel {
32    /// LED lights when pin is HIGH (standard wiring).
33    /// LED anode → 220Ω resistor → GPIO pin, LED cathode → GND
34    #[default]
35    High,
36
37    /// LED lights when pin is LOW (alternative wiring).
38    /// LED anode → 3.3V, LED cathode → 220Ω resistor → GPIO pin
39    Low,
40}
41
42// ============================================================================
43// LedCommand Enum
44// ============================================================================
45
46#[derive(Clone)]
47pub(crate) enum LedCommand {
48    /// Set LED level immediately.
49    Set(Level),
50    /// Play an animation sequence (looping).
51    Animate(Vec<(Level, Duration), MAX_FRAMES>),
52}
53
54// ============================================================================
55// Led Virtual Device
56// ============================================================================
57
58/// A device abstraction for a single digital LED with animation support.
59///
60/// # Hardware Requirements
61///
62/// This device requires a single GPIO pin connected to an LED. The LED can be wired
63/// for either active-high (default) or active-low operation. The device supports both
64/// polarities and controls the pin internally.
65///
66/// **Active-high wiring (default):** LED anode (long leg) → 220Ω resistor → GPIO pin, LED cathode (short leg) → GND
67/// **Active-low wiring:** LED anode (long leg) → 3.3V, LED cathode (short leg) → 220Ω resistor → GPIO pin
68///
69/// # Example
70///
71/// ```rust,no_run
72/// # #![no_std]
73/// # #![no_main]
74/// use device_envoy::{Result, led::{Led, LedStatic, OnLevel}};
75/// use embassy_time::Duration;
76/// use embassy_rp::gpio::Level;
77/// # #[panic_handler]
78/// # fn panic(_info: &core::panic::PanicInfo) -> ! { loop {} }
79///
80/// async fn example(p: embassy_rp::Peripherals, spawner: embassy_executor::Spawner) -> Result<()> {
81///     static LED_STATIC: LedStatic = Led::new_static();
82///     let led = Led::new(&LED_STATIC, p.PIN_1, OnLevel::High, spawner)?;
83///
84///     // Turn the LED on
85///     led.set_level(Level::High);
86///     embassy_time::Timer::after(Duration::from_secs(1)).await;
87///
88///     // Turn the LED off
89///     led.set_level(Level::Low);
90///     embassy_time::Timer::after(Duration::from_millis(500)).await;
91///
92///     // Play a blinking animation (looping: 200ms on, 200ms off)
93///     led.animate(&[(Level::High, Duration::from_millis(200)), (Level::Low, Duration::from_millis(200))]);
94///
95///     core::future::pending().await // run forever
96/// }
97/// ```
98///
99/// The device runs a background task that handles state transitions and animations.
100/// Create the device once with [`Led::new`] and use the returned handle for all updates.
101pub struct Led<'a>(&'a LedOuterStatic);
102
103/// Signal for sending LED commands to the [`Led`] device.
104pub(crate) type LedOuterStatic = Signal<CriticalSectionRawMutex, LedCommand>;
105
106/// Static resources for the [`Led`] device.
107pub struct LedStatic {
108    outer: LedOuterStatic,
109}
110
111impl LedStatic {
112    /// Creates static resources for a single LED device.
113    pub(crate) const fn new() -> Self {
114        Self {
115            outer: Signal::new(),
116        }
117    }
118}
119
120impl Led<'_> {
121    /// Creates a single LED device and spawns its background task; see [`Led`] docs.
122    #[must_use = "Must be used to manage the spawned task"]
123    pub fn new<P: embassy_rp::gpio::Pin>(
124        led_static: &'static LedStatic,
125        pin: Peri<'static, P>,
126        on_level: OnLevel,
127        spawner: Spawner,
128    ) -> Result<Self> {
129        let pin_output = Output::new(pin, Level::Low);
130        let token = device_loop(&led_static.outer, pin_output, on_level);
131        spawner.spawn(token).map_err(Error::TaskSpawn)?;
132        Ok(Self(&led_static.outer))
133    }
134
135    /// Creates static resources for [`Led::new`]; see [`Led`] docs.
136    #[must_use]
137    pub const fn new_static() -> LedStatic {
138        LedStatic::new()
139    }
140
141    /// Set the LED level immediately, replacing any running animation.
142    ///
143    /// See [Led struct example](Self) for usage.
144    pub fn set_level(&self, level: Level) {
145        self.0.signal(LedCommand::Set(level));
146    }
147
148    /// Play a looped animation sequence of LED levels with durations.
149    ///
150    /// Accepts any iterator yielding (Level, Duration) pairs or references, up to 32 frames.
151    /// The animation will loop continuously until replaced by another command.
152    /// See [Led struct example](Self) for usage.
153    pub fn animate<I>(&self, frames: I)
154    where
155        I: IntoIterator,
156        I::Item: Borrow<(Level, Duration)>,
157    {
158        let mut animation: Vec<(Level, Duration), MAX_FRAMES> = Vec::new();
159        for frame in frames {
160            let frame = *frame.borrow();
161            animation
162                .push(frame)
163                .expect("LED animation fits within MAX_FRAMES");
164        }
165        self.0.signal(LedCommand::Animate(animation));
166    }
167}
168
169#[embassy_executor::task]
170async fn device_loop(
171    outer_static: &'static LedOuterStatic,
172    mut pin: Output<'static>,
173    on_level: OnLevel,
174) -> ! {
175    let mut command = LedCommand::Set(Level::Low);
176    set_pin_for_led_level(Level::Low, &mut pin, on_level);
177
178    loop {
179        command = match command {
180            LedCommand::Set(level) => {
181                run_set_level_loop(level, outer_static, &mut pin, on_level).await
182            }
183            LedCommand::Animate(animation) => {
184                run_animation_loop(animation, outer_static, &mut pin, on_level).await
185            }
186        };
187    }
188}
189
190/// Set the physical pin state based on desired LED level and on_level.
191fn set_pin_for_led_level(led_level: Level, pin: &mut Output<'_>, on_level: OnLevel) {
192    let pin_level = match (led_level, on_level) {
193        (Level::High, OnLevel::High) | (Level::Low, OnLevel::Low) => Level::High,
194        (Level::Low, OnLevel::High) | (Level::High, OnLevel::Low) => Level::Low,
195    };
196    pin.set_level(pin_level);
197}
198
199async fn run_set_level_loop(
200    level: Level,
201    outer_static: &'static LedOuterStatic,
202    pin: &mut Output<'_>,
203    on_level: OnLevel,
204) -> LedCommand {
205    set_pin_for_led_level(level, pin, on_level);
206
207    loop {
208        match outer_static.wait().await {
209            LedCommand::Set(new_level) => {
210                if new_level == level {
211                    // No change, keep waiting
212                    continue;
213                } else {
214                    return LedCommand::Set(new_level);
215                }
216            }
217            other => return other,
218        }
219    }
220}
221
222async fn run_animation_loop(
223    animation: Vec<(Level, Duration), MAX_FRAMES>,
224    outer_static: &'static LedOuterStatic,
225    pin: &mut Output<'_>,
226    on_level: OnLevel,
227) -> LedCommand {
228    if animation.is_empty() {
229        return LedCommand::Animate(animation);
230    }
231
232    let mut frame_index = 0;
233
234    loop {
235        let (level, duration) = animation[frame_index];
236
237        set_pin_for_led_level(level, pin, on_level);
238
239        frame_index = (frame_index + 1) % animation.len();
240
241        // Wait for duration, but check for new commands
242        match embassy_futures::select::select(Timer::after(duration), outer_static.wait()).await {
243            embassy_futures::select::Either::First(_) => {
244                // Duration elapsed, continue animation
245            }
246            embassy_futures::select::Either::Second(command) => {
247                // New command received
248                return command;
249            }
250        }
251    }
252}