Skip to main content

device_envoy_esp/
led.rs

1//! A device abstraction for a single digital LED with animation support.
2//!
3//! Use the [`led!`](macro@crate::led) macro to generate one or more concrete LED
4//! device types.
5//!
6//! See [`LedGenerated`](led_generated::LedGenerated) for a sample generated type.
7//!
8//! # Example
9//!
10//! ```rust,no_run
11//! # #![no_std]
12//! # #![no_main]
13//! use device_envoy_esp::{
14//!     Result,
15//!     init_and_start,
16//!     led,
17//!     led::{Led as _, LedLevel, OnLevel},
18//! };
19//! use embassy_time::Duration;
20//! # #[panic_handler]
21//! # fn panic(_info: &core::panic::PanicInfo) -> ! { loop {} }
22//!
23//! led! {
24//!     pub LedOne {
25//!         pin: GPIO2
26//!     }
27//! }
28//! led! {
29//!     pub LedTwo {
30//!         pin: GPIO3,
31//!         max_steps: 2
32//!     }
33//! }
34//!
35//! async fn example(spawner: embassy_executor::Spawner) -> Result<()> {
36//!     init_and_start!(p);
37//!     let led_one = LedOne::new(p.GPIO2, OnLevel::High, spawner)?;
38//!     let led_two = LedTwo::new(p.GPIO3, OnLevel::High, spawner)?;
39//!
40//!     led_one.set_level(LedLevel::On);
41//!     led_two.set_level(LedLevel::Off);
42//!     embassy_time::Timer::after(Duration::from_millis(250)).await;
43//!
44//!     led_one.animate([
45//!         (LedLevel::On, Duration::from_millis(200)),
46//!         (LedLevel::Off, Duration::from_millis(200)),
47//!     ]);
48//!     led_two.animate([
49//!         (LedLevel::Off, Duration::from_millis(150)),
50//!         (LedLevel::On, Duration::from_millis(150)),
51//!     ]);
52//!
53//!     core::future::pending().await
54//! }
55//! ```
56
57pub use device_envoy_core::led::{Led, LedLevel, OnLevel};
58pub mod led_generated;
59#[cfg(target_os = "none")]
60#[doc(hidden)]
61pub use paste;
62
63#[cfg(target_os = "none")]
64use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal};
65#[cfg(target_os = "none")]
66use embassy_time::{Duration, Timer};
67#[cfg(target_os = "none")]
68use esp_hal::gpio::{Level, Output};
69#[cfg(target_os = "none")]
70use heapless::Vec;
71
72#[cfg(target_os = "none")]
73#[doc(hidden)] // Public for macro expansion in downstream crates; not a user-facing API.
74pub const DEFAULT_MAX_STEPS: usize = 32;
75
76#[cfg(target_os = "none")]
77#[derive(Clone)]
78#[doc(hidden)] // Public for macro expansion in downstream crates; not a user-facing API.
79pub enum LedCommand<const MAX_STEPS: usize> {
80    Set(LedLevel),
81    Animate(Vec<(LedLevel, Duration), MAX_STEPS>),
82}
83
84#[cfg(target_os = "none")]
85#[doc(hidden)] // Public for macro expansion in downstream crates; not a user-facing API.
86pub type LedOuterStatic<const MAX_STEPS: usize> =
87    Signal<CriticalSectionRawMutex, LedCommand<MAX_STEPS>>;
88
89#[cfg(target_os = "none")]
90#[doc(hidden)] // Public for macro expansion in downstream crates; not a user-facing API.
91pub struct LedStatic<const MAX_STEPS: usize> {
92    outer: LedOuterStatic<MAX_STEPS>,
93}
94
95#[cfg(target_os = "none")]
96impl<const MAX_STEPS: usize> LedStatic<MAX_STEPS> {
97    #[doc(hidden)] // Public for macro expansion in downstream crates; not a user-facing API.
98    pub const fn new() -> Self {
99        Self {
100            outer: Signal::new(),
101        }
102    }
103
104    #[doc(hidden)] // Public for macro expansion in downstream crates; not a user-facing API.
105    pub fn outer(&self) -> &LedOuterStatic<MAX_STEPS> {
106        &self.outer
107    }
108}
109
110#[cfg(target_os = "none")]
111#[doc(hidden)] // Public for macro expansion in downstream crates; not a user-facing API.
112pub fn set_pin_for_led_level(led_level: LedLevel, pin: &mut Output<'_>, on_level: OnLevel) {
113    let pin_level = match (led_level, on_level) {
114        (LedLevel::On, OnLevel::High) | (LedLevel::Off, OnLevel::Low) => Level::High,
115        (LedLevel::Off, OnLevel::High) | (LedLevel::On, OnLevel::Low) => Level::Low,
116    };
117    pin.set_level(pin_level);
118}
119
120#[cfg(target_os = "none")]
121#[doc(hidden)] // Public for macro expansion in downstream crates; not a user-facing API.
122pub async fn run_set_level_loop<const MAX_STEPS: usize>(
123    led_level: LedLevel,
124    outer_static: &'static LedOuterStatic<MAX_STEPS>,
125    pin: &mut Output<'_>,
126    on_level: OnLevel,
127) -> LedCommand<MAX_STEPS> {
128    set_pin_for_led_level(led_level, pin, on_level);
129
130    loop {
131        match outer_static.wait().await {
132            LedCommand::Set(new_led_level) => {
133                if new_led_level == led_level {
134                    continue;
135                }
136                return LedCommand::Set(new_led_level);
137            }
138            other => return other,
139        }
140    }
141}
142
143#[cfg(target_os = "none")]
144#[doc(hidden)] // Public for macro expansion in downstream crates; not a user-facing API.
145pub async fn run_animation_loop<const MAX_STEPS: usize>(
146    animation: Vec<(LedLevel, Duration), MAX_STEPS>,
147    outer_static: &'static LedOuterStatic<MAX_STEPS>,
148    pin: &mut Output<'_>,
149    on_level: OnLevel,
150) -> LedCommand<MAX_STEPS> {
151    if animation.is_empty() {
152        return LedCommand::Animate(animation);
153    }
154
155    let mut frame_index = 0;
156
157    loop {
158        let (led_level, duration) = animation[frame_index];
159
160        set_pin_for_led_level(led_level, pin, on_level);
161
162        frame_index = (frame_index + 1) % animation.len();
163
164        match embassy_futures::select::select(Timer::after(duration), outer_static.wait()).await {
165            embassy_futures::select::Either::First(_) => {}
166            embassy_futures::select::Either::Second(command) => return command,
167        }
168    }
169}
170
171/// Macro to generate a single LED struct type (includes syntax details).
172///
173/// **See the [led module documentation](mod@crate::led) for usage examples.**
174///
175/// **Syntax:**
176///
177/// ```text
178/// led! {
179///     [<visibility>] <Name> {
180///         pin: <pin_ident>,
181///         max_steps: <usize_expr>, // optional
182///     }
183/// }
184/// ```
185///
186/// **Required fields:**
187///
188/// - `pin` - GPIO pin resource type for this generated LED.
189///
190/// **Optional fields:**
191///
192/// - `max_steps` - Maximum number of animation frames (default: 32).
193///
194/// `max_steps = 0` disables animation storage; `set_level()` is still supported.
195#[cfg(target_os = "none")]
196#[doc(hidden)]
197#[macro_export]
198macro_rules! led {
199    ($($tt:tt)*) => { $crate::__led_impl! { $($tt)* } };
200}
201
202/// Implementation macro. Not part of the public API; use [`led!`] instead.
203#[cfg(target_os = "none")]
204#[doc(hidden)]
205#[macro_export]
206macro_rules! __led_impl {
207    (
208        $vis:vis $name:ident {
209            $($fields:tt)*
210        }
211    ) => {
212        $crate::__led_impl! {
213            @__parse
214            vis: $vis,
215            name: $name,
216            pin: [],
217            max_steps: [],
218            fields: [ $($fields)* ]
219        }
220    };
221
222    (@__parse
223        vis: $vis:vis,
224        name: $name:ident,
225        pin: [],
226        max_steps: [$($max_steps:expr)?],
227        fields: [ pin: $pin:ident $(, $($rest:tt)*)? ]
228    ) => {
229        $crate::__led_impl! {
230            @__parse
231            vis: $vis,
232            name: $name,
233            pin: [$pin],
234            max_steps: [$($max_steps)?],
235            fields: [ $($($rest)*)? ]
236        }
237    };
238    (@__parse
239        vis: $vis:vis,
240        name: $name:ident,
241        pin: [$_pin_seen:ident],
242        max_steps: [$($max_steps:expr)?],
243        fields: [ pin: $pin:ident $(, $($rest:tt)*)? ]
244    ) => {
245        compile_error!("led! duplicate `pin` field");
246    };
247
248    (@__parse
249        vis: $vis:vis,
250        name: $name:ident,
251        pin: [$($pin:ident)?],
252        max_steps: [],
253        fields: [ max_steps: $max_steps:expr $(, $($rest:tt)*)? ]
254    ) => {
255        $crate::__led_impl! {
256            @__parse
257            vis: $vis,
258            name: $name,
259            pin: [$($pin)?],
260            max_steps: [$max_steps],
261            fields: [ $($($rest)*)? ]
262        }
263    };
264    (@__parse
265        vis: $vis:vis,
266        name: $name:ident,
267        pin: [$($pin:ident)?],
268        max_steps: [$_max_steps_seen:expr],
269        fields: [ max_steps: $max_steps:expr $(, $($rest:tt)*)? ]
270    ) => {
271        compile_error!("led! duplicate `max_steps` field");
272    };
273
274    (@__parse
275        vis: $vis:vis,
276        name: $name:ident,
277        pin: [$($pin:ident)?],
278        max_steps: [$($max_steps:expr)?],
279        fields: [ ]
280    ) => {
281        $crate::__led_impl! {
282            @__finish
283            vis: $vis,
284            name: $name,
285            pin: [$($pin)?],
286            max_steps: [$($max_steps)?]
287        }
288    };
289
290    (@__parse
291        vis: $vis:vis,
292        name: $name:ident,
293        pin: [$($pin:ident)?],
294        max_steps: [$($max_steps:expr)?],
295        fields: [ $field:ident : $value:expr $(, $($rest:tt)*)? ]
296    ) => {
297        compile_error!("led! unknown field; expected `pin` or `max_steps`");
298    };
299
300    (@__finish
301        vis: $vis:vis,
302        name: $name:ident,
303        pin: [],
304        max_steps: [$($max_steps:expr)?]
305    ) => {
306        compile_error!("led! missing required `pin` field");
307    };
308
309    (@__finish
310        vis: $vis:vis,
311        name: $name:ident,
312        pin: [$pin:ident],
313        max_steps: []
314    ) => {
315        $crate::__led_impl!(@__emit vis: $vis, name: $name, pin: $pin, max_steps: $crate::led::DEFAULT_MAX_STEPS);
316    };
317
318    (@__finish
319        vis: $vis:vis,
320        name: $name:ident,
321        pin: [$pin:ident],
322        max_steps: [$max_steps:expr]
323    ) => {
324        $crate::__led_impl!(@__emit vis: $vis, name: $name, pin: $pin, max_steps: $max_steps);
325    };
326
327    (
328        @__emit
329        vis: $vis:vis,
330        name: $name:ident,
331        pin: $pin:ident,
332        max_steps: $max_steps:expr
333    ) => {
334        $crate::led::paste::paste! {
335            #[cfg(target_os = "none")]
336            const [<$name:upper _MAX_STEPS>]: usize = $max_steps;
337
338            #[cfg(target_os = "none")]
339            #[allow(non_upper_case_globals)]
340            static [<$name:upper _STATIC>]: $crate::led::LedStatic<{ [<$name:upper _MAX_STEPS>] }> =
341                $crate::led::LedStatic::new();
342
343            #[cfg(target_os = "none")]
344            #[allow(non_camel_case_types)]
345            $vis struct $name(&'static $crate::led::LedOuterStatic<{ [<$name:upper _MAX_STEPS>] }>);
346
347            #[cfg(target_os = "none")]
348            impl $name {
349                $vis const MAX_STEPS: usize = [<$name:upper _MAX_STEPS>];
350
351                pub fn new(
352                    pin: $crate::esp_hal::peripherals::$pin<'static>,
353                    on_level: $crate::led::OnLevel,
354                    spawner: embassy_executor::Spawner,
355                ) -> $crate::Result<Self> {
356                    let pin_output = $crate::esp_hal::gpio::Output::new(
357                        pin,
358                        $crate::esp_hal::gpio::Level::Low,
359                        $crate::esp_hal::gpio::OutputConfig::default(),
360                    );
361                    let token = [<__led_task_ $name:snake>](
362                        [<$name:upper _STATIC>].outer(),
363                        pin_output,
364                        on_level,
365                    );
366                    spawner.spawn(token).map_err($crate::Error::TaskSpawn)?;
367                    Ok(Self([<$name:upper _STATIC>].outer()))
368                }
369            }
370
371            #[cfg(target_os = "none")]
372            impl $crate::led::Led for $name {
373                fn set_level(&self, led_level: $crate::led::LedLevel) {
374                    self.0.signal($crate::led::LedCommand::Set(led_level));
375                }
376
377                fn animate<I>(&self, frames: I)
378                where
379                    I: IntoIterator,
380                    I::Item: ::core::borrow::Borrow<(
381                        $crate::led::LedLevel,
382                        embassy_time::Duration,
383                    )>,
384                {
385                    let mut animation: heapless::Vec<
386                        ($crate::led::LedLevel, embassy_time::Duration),
387                        { [<$name:upper _MAX_STEPS>] },
388                    > = heapless::Vec::new();
389                    for frame in frames {
390                        let frame = *::core::borrow::Borrow::borrow(&frame);
391                        animation
392                            .push(frame)
393                            .expect("LED animation fits within MAX_STEPS");
394                    }
395                    self.0.signal($crate::led::LedCommand::Animate(animation));
396                }
397            }
398
399            #[cfg(target_os = "none")]
400            #[embassy_executor::task]
401            async fn [<__led_task_ $name:snake>](
402                outer_static: &'static $crate::led::LedOuterStatic<{ [<$name:upper _MAX_STEPS>] }>,
403                mut pin: $crate::esp_hal::gpio::Output<'static>,
404                on_level: $crate::led::OnLevel,
405            ) -> ! {
406                let mut command = $crate::led::LedCommand::Set($crate::led::LedLevel::Off);
407                $crate::led::set_pin_for_led_level($crate::led::LedLevel::Off, &mut pin, on_level);
408
409                loop {
410                    command = match command {
411                        $crate::led::LedCommand::Set(led_level) => {
412                            $crate::led::run_set_level_loop(led_level, outer_static, &mut pin, on_level).await
413                        }
414                        $crate::led::LedCommand::Animate(animation) => {
415                            $crate::led::run_animation_loop(animation, outer_static, &mut pin, on_level).await
416                        }
417                    };
418                }
419            }
420        }
421    };
422}
423
424#[cfg(target_os = "none")]
425#[doc(inline)]
426pub use led;