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}