Skip to main content

device_envoy_esp/
led_strip.rs

1#![cfg_attr(
2    feature = "doc-images",
3    doc = ::embed_doc_image::embed_image!(
4        "led_strip_simple",
5        "docs/assets/led_strip_simple.png"
6    ),
7    doc = ::embed_doc_image::embed_image!(
8        "led_strip_animated",
9        "docs/assets/led_strip_animated.png"
10    )
11)]
12//! A device abstraction for 1-dimensional NeoPixel-style (WS2812) LED strips. For 2-dimensional
13//! panels, see the [`led2d`](mod@crate::led2d) module.
14//!
15//! This page provides the primary documentation and examples for programming LED strips.
16//! The device abstraction supports pixel patterns and animation on the LED strip.
17//!
18//! **After reading the examples below, see also:**
19//!
20//! - [`led_strip!`](macro@crate::led_strip) - Macro to generate an LED-strip struct type (includes syntax details).
21//! - [`LedStrip`](`crate::led_strip::LedStrip`) - Core trait defining the LED strip API surface.
22//! - [`LedStripGenerated`](led_strip_generated::LedStripGenerated) - Sample generated strip type showing the constructor path.
23//! - [`Frame1d`] - 1D pixel array used to describe LED strip patterns.
24//!
25//! # Example: Write a Single 1-Dimensional Frame
26//!
27//! In this example, we set every other LED to blue and gray. Here, the generated struct type is
28//! named `LedStripSimple`.
29//!
30//! ![LED strip preview][led_strip_simple]
31//!
32//! ```rust,no_run
33//! # #![no_std]
34//! # #![no_main]
35//! # use core::convert::Infallible;
36//! # use esp_backtrace as _;
37//! use device_envoy_esp::{Result, init_and_start, led_strip, led_strip::{Frame1d, LedStrip as _, colors}};
38//!
39//! // Define LedStripSimple, a struct type for an 8-LED strip on GPIO8.
40//! led_strip! {
41//!     LedStripSimple {
42//!         pin: GPIO8,  // GPIO pin for LED data
43//!         len: 8,      // 8 LEDs
44//!         // other inputs set to their defaults
45//!     }
46//! }
47//!
48//! # #[esp_rtos::main]
49//! # async fn main(spawner: embassy_executor::Spawner) -> ! {
50//! #     let err = example(spawner).await.unwrap_err();
51//! #     panic!("{err:?}");
52//! # }
53//! async fn example(spawner: embassy_executor::Spawner) -> Result<Infallible> {
54//!     init_and_start!(p, rmt80: rmt80, mode: rmt_mode::Blocking);
55//!     // Create a LedStripSimple instance.
56//!     let led_strip_simple = LedStripSimple::new(p.GPIO8, rmt80.channel0, spawner)?;
57//!
58//!     // Create and write a frame with alternating blue and gray pixels.
59//!     let mut frame = Frame1d::new();
60//!     for pixel_index in 0..LedStripSimple::LEN {
61//!         // Directly index into the frame buffer.
62//!         frame[pixel_index] = [colors::BLUE, colors::GRAY][pixel_index % 2];
63//!     }
64//!
65//!     // Display the frame on the LED strip (until replaced).
66//!     led_strip_simple.write_frame(frame);
67//!
68//!     core::future::pending().await
69//! }
70//! ```
71//!
72//! # Example: Animate a Sequence
73//!
74//! This example animates a 96-LED strip through red, green, and blue frames, cycling continuously.
75//! Here, the generated struct type is named `LedStripAnimated`.
76//!
77//! ![LED strip preview][led_strip_animated]
78//!
79//! ```rust,no_run
80//! # #![no_std]
81//! # #![no_main]
82//! # use core::convert::Infallible;
83//! # use esp_backtrace as _;
84//! use device_envoy_esp::{Result, init_and_start, led_strip, led_strip::{Current, Frame1d, Gamma, LedStrip as _, colors}};
85//! use embassy_time::Duration;
86//!
87//! // Define LedStripAnimated, a struct type for a 96-LED strip on GPIO18.
88//! // We change some defaults including setting a 1A power budget and disabling gamma correction.
89//! led_strip! {
90//!     LedStripAnimated {
91//!         pin: GPIO18,                           // GPIO pin for LED data
92//!         len: 96,                               // 96 LEDs
93//!         max_current: Current::Milliamps(1000), // 1A power budget
94//!         gamma: Gamma::Linear,                  // No color correction
95//!         max_frames: 3,                         // Up to 3 animation frames
96//!     }
97//! }
98//!
99//! # #[esp_rtos::main]
100//! # async fn main(spawner: embassy_executor::Spawner) -> ! {
101//! #     let err = example(spawner).await.unwrap_err();
102//! #     panic!("{err:?}");
103//! # }
104//! async fn example(spawner: embassy_executor::Spawner) -> Result<Infallible> {
105//!     init_and_start!(p, rmt80: rmt80, mode: rmt_mode::Blocking);
106//!     let led_strip_animated = LedStripAnimated::new(p.GPIO18, rmt80.channel0, spawner)?;
107//!
108//!     // Create a sequence of frames and durations and then animate them (looping, until replaced).
109//!     let frame_duration = Duration::from_millis(300);
110//!     led_strip_animated.animate([
111//!         (Frame1d::filled(colors::RED), frame_duration),
112//!         (Frame1d::filled(colors::GREEN), frame_duration),
113//!         (Frame1d::filled(colors::BLUE), frame_duration),
114//!     ]);
115//!
116//!     core::future::pending().await
117//! }
118//! ```
119
120pub use device_envoy_core::led_strip::*;
121pub mod led_strip_generated;
122
123/// Internal runtime handle for macro-generated LED strip types.
124///
125/// `#[doc(hidden)]` because this is implementation detail used by macro output.
126#[doc(hidden)]
127pub struct LedStripEsp<const N: usize, const MAX_FRAMES: usize> {
128    command_signal: &'static LedStripCommandSignal<N, MAX_FRAMES>,
129}
130
131impl<const N: usize, const MAX_FRAMES: usize> LedStripEsp<N, MAX_FRAMES> {
132    #[doc(hidden)]
133    pub const fn new_static() -> LedStripStatic<N, MAX_FRAMES> {
134        LedStripStatic::new_static()
135    }
136
137    #[doc(hidden)]
138    pub fn new(led_strip_static: &'static LedStripStatic<N, MAX_FRAMES>) -> Self {
139        Self {
140            command_signal: led_strip_static.command_signal(),
141        }
142    }
143
144    // Must be `pub` for macro expansion at foreign call sites — not user-facing.
145    #[doc(hidden)]
146    pub fn __command_signal(&self) -> &'static LedStripCommandSignal<N, MAX_FRAMES> {
147        self.command_signal
148    }
149}
150
151/// Tells whether to run LEDs from an [RMT resource](crate#glossary) or an
152/// [SPI resource](crate#glossary).
153#[derive(Clone, Copy, Debug, Eq, PartialEq)]
154pub enum Engine {
155    /// Use an [RMT resource](crate#glossary).
156    Rmt,
157    /// Use an [SPI resource](crate#glossary).
158    Spi,
159}
160
161impl Default for Engine {
162    fn default() -> Self {
163        #[cfg(esp_has_rmt)]
164        {
165            return Self::Rmt;
166        }
167        #[cfg(not(esp_has_rmt))]
168        {
169            return Self::Spi;
170        }
171        #[allow(unreachable_code)]
172        Self::Rmt
173    }
174}
175
176// Must be `pub` for macro expansion at foreign call sites.
177// This is an implementation detail, not part of the user-facing API.
178#[doc(hidden)]
179/// Default current budget used by [`led_strip!`](macro@crate::led_strip) when
180/// `max_current` is omitted.
181pub const CURRENT_DEFAULT: Current = Current::Milliamps(250);
182
183// ============================================================================
184// RMT driver (ESP32-specific)
185// ============================================================================
186
187#[cfg(all(target_os = "none", esp_has_rmt))]
188use embassy_futures::select::{Either, select};
189#[cfg(all(target_os = "none", esp_has_rmt))]
190use embassy_time::Timer;
191
192#[cfg(all(target_os = "none", esp_has_rmt))]
193use esp_hal::gpio::Level;
194#[cfg(all(target_os = "none", esp_has_rmt))]
195use esp_hal::rmt::{Channel, PulseCode, Tx};
196
197// WS2812 timing at 80 MHz RMT clock with clk_divider=4 → 50 ns per tick.
198//   T0H =  0.4 µs  → 8 ticks     T0L = 0.85 µs → 17 ticks
199//   T1H =  0.8 µs  → 16 ticks    T1L = 0.45 µs →  9 ticks
200#[cfg(all(target_os = "none", esp_has_rmt))]
201const BIT0: PulseCode = PulseCode::new(Level::High, 8, Level::Low, 17);
202#[cfg(all(target_os = "none", esp_has_rmt))]
203const BIT1: PulseCode = PulseCode::new(Level::High, 16, Level::Low, 9);
204
205/// WS2812 driver backed by an ESP32 RMT TX channel.
206///
207/// `LEDS` is the number of LED pixels; `PULSES` must equal `LEDS * 24 + 1`.
208/// Both are generated as concrete `const` values by the [`led_strip!`](macro@crate::led_strip) macro,
209/// so no `generic_const_exprs` is required.
210///
211/// The pulse buffer is a **field** of this struct so that it lives in BSS /
212/// static memory rather than on the stack.
213#[cfg(all(target_os = "none", esp_has_rmt))]
214// Must be `pub` for macro expansion at foreign call sites.
215// This is an implementation detail, not part of the user-facing API.
216#[doc(hidden)]
217pub struct RmtWs2812<'d, const LEDS: usize, const PULSES: usize> {
218    channel: Option<Channel<'d, esp_hal::Blocking, Tx>>,
219    pulse_buf: [PulseCode; PULSES],
220}
221
222#[cfg(all(target_os = "none", esp_has_rmt))]
223impl<'d, const LEDS: usize, const PULSES: usize> RmtWs2812<'d, LEDS, PULSES> {
224    /// Create a new driver, taking ownership of an RMT TX channel.
225    ///
226    /// Called internally by the `led_strip!`-generated `new()`. The channel
227    /// must be configured with clock divider 4, no carrier, and idle-low.
228    #[must_use]
229    pub fn new(channel: Channel<'d, esp_hal::Blocking, Tx>) -> Self {
230        assert_eq!(
231            PULSES,
232            LEDS * 24 + 1,
233            "PULSES must equal LEDS * 24 + 1; this is enforced by led_strip!"
234        );
235        Self {
236            channel: Some(channel),
237            pulse_buf: [PulseCode::end_marker(); PULSES],
238        }
239    }
240
241    /// Encode `frame` into the pulse buffer and transmit synchronously.
242    ///
243    /// GRB byte order (required by WS2812) is applied here. Gamma/brightness
244    /// correction must be applied to the frame before calling this method.
245    pub fn write(&mut self, frame: &Frame1d<LEDS>) -> Result<(), Error> {
246        // Encode each pixel as 24 bits in GRB MSB-first order.
247        for (led_index, pixel) in frame.iter().enumerate() {
248            let grb: u32 = ((pixel.g as u32) << 16) | ((pixel.r as u32) << 8) | (pixel.b as u32);
249            for bit_index in 0..24 {
250                let bit = (grb >> (23 - bit_index)) & 1;
251                self.pulse_buf[led_index * 24 + bit_index] = if bit == 1 { BIT1 } else { BIT0 };
252            }
253        }
254        // Final slot is always the end marker. Written explicitly on every
255        // transmit to guard against future refactoring.
256        self.pulse_buf[LEDS * 24] = PulseCode::end_marker();
257
258        let channel = self.channel.take().ok_or(Error::ChannelMissing)?;
259        let transfer = channel
260            .transmit(&self.pulse_buf)
261            .map_err(|_| Error::TransmitStart)?;
262        match transfer.wait() {
263            Ok(channel) => {
264                self.channel = Some(channel);
265                Ok(())
266            }
267            Err((err, channel)) => {
268                self.channel = Some(channel);
269                Err(Error::Transmit(err))
270            }
271        }
272    }
273}
274
275#[cfg(all(target_os = "none", esp_has_rmt))]
276#[doc(hidden)]
277/// Errors returned by [`RmtWs2812::write`].
278#[derive(Debug)]
279pub enum Error {
280    /// Channel was already consumed and not recovered (internal logic error).
281    ChannelMissing,
282    /// RMT peripheral could not start the transfer.
283    TransmitStart,
284    /// RMT peripheral reported an error during or after transfer.
285    Transmit(esp_hal::rmt::Error),
286}
287
288// ============================================================================
289// Device loop
290// ============================================================================
291
292/// Asynchronous device loop for a WS2812 LED strip.
293///
294/// Call this from an `embassy_executor::task` spawned by the generated
295/// `new()` constructor. It runs forever, receiving [`Command`]s from the
296/// matching [`LedStrip`] handle.
297///
298/// `#[doc(hidden)]` — called exclusively from macro-generated task code.
299#[doc(hidden)]
300#[cfg(all(target_os = "none", esp_has_rmt))]
301pub async fn led_strip_device_loop<
302    'd,
303    const LEDS: usize,
304    const PULSES: usize,
305    const MAX_FRAMES: usize,
306>(
307    mut driver: RmtWs2812<'d, LEDS, PULSES>,
308    command_signal: &'static LedStripCommandSignal<LEDS, MAX_FRAMES>,
309    combo_table: &'static [u8; 256],
310) -> ! {
311    // Start with all LEDs off.
312    let _ = driver.write(&Frame1d::new());
313
314    // `pending` carries a command that was received during animation into the
315    // next iteration of the outer loop, avoiding recursion.
316    let mut pending: Option<Command<LEDS, MAX_FRAMES>> = None;
317
318    loop {
319        let command = match pending.take() {
320            Some(cmd) => cmd,
321            None => command_signal.wait().await,
322        };
323
324        match command {
325            Command::DisplayStatic(mut frame) => {
326                apply_correction(&mut frame, combo_table);
327                let _ = driver.write(&frame);
328                // Hold until the next command arrives — handled at the top of the loop.
329            }
330            Command::Animate(sequence) => {
331                // Loop the animation sequence until interrupted by a new command.
332                'animate: loop {
333                    for (mut frame, duration) in sequence.iter().cloned() {
334                        apply_correction(&mut frame, combo_table);
335                        let _ = driver.write(&frame);
336                        match select(Timer::after(duration), command_signal.wait()).await {
337                            Either::First(_) => {
338                                // Timer elapsed — continue to next frame.
339                            }
340                            Either::Second(new_command) => {
341                                // New command arrived mid-animation; carry it to the
342                                // outer loop via `pending` rather than recurse.
343                                pending = Some(new_command);
344                                break 'animate;
345                            }
346                        }
347                    }
348                    // One full pass completed — check for a new command before
349                    // looping the animation again (non-blocking).
350                    if let Some(new_command) = command_signal.try_take() {
351                        pending = Some(new_command);
352                        break 'animate;
353                    }
354                }
355            }
356        }
357    }
358}
359
360// ============================================================================
361// led_strip! macro
362// ============================================================================
363
364/// Macro to generate an LED-strip struct type (includes syntax details).
365///
366/// **See the [led_strip module documentation](mod@crate::led_strip) for usage examples.**
367///
368/// **Syntax:**
369///
370/// ```text
371/// led_strip! {
372///     [<visibility>] <Name> {
373///         pin: <pin_ident>,
374///         len: <usize_expr>,
375///         max_current: <Current_expr>, // optional
376///         engine: <Engine_expr>,       // optional
377///         gamma: <Gamma_expr>,         // optional
378///         max_frames: <usize_expr>,    // optional
379///         reset_us: <u32_expr>,        // optional (SPI only)
380///     }
381/// }
382/// ```
383///
384/// **Required fields:**
385///
386/// - `pin` — GPIO pin for LED data
387/// - `len` — Number of LEDs
388///
389/// **Optional fields:**
390///
391/// - `max_current` — Electrical current budget (default: 250 mA)
392/// - `engine` — Output engine (default: `Engine::Rmt` on RMT-capable chips, otherwise `Engine::Spi`)
393/// - `gamma` — Color curve (default: `Gamma::Srgb`)
394/// - `max_frames` — Maximum number of animation frames (default: 16 frames)
395/// - `reset_us` — WS2812 reset/latch interval in microseconds for `Engine::Spi` (default: 60)
396///
397/// `max_frames = 0` disables animation and allocates no frame storage; `write_frame()` is still supported.
398///
399#[doc = include_str!("docs/current_limiting_and_gamma.md")]
400///
401/// # Related Macros
402///
403/// - [`led2d!`](mod@crate::led2d) — For 2-dimensional LED panels
404#[doc(hidden)]
405#[macro_export]
406macro_rules! led_strip {
407    ($($tt:tt)*) => { $crate::__led_strip_entry! { $($tt)* } };
408}
409
410/// Implementation macro. Not part of the public API; use [`led_strip!`] instead.
411#[doc(hidden)]
412#[macro_export]
413macro_rules! __led_strip_entry {
414    (
415        $name:ident {
416            $($before:tt)*
417            led2d: { $($led2d_fields:tt)* }
418            $($after:tt)*
419        }
420    ) => {
421        compile_error!("led_strip! is 1D-only. Use led2d! for panel generation.");
422    };
423    (
424        $name:ident {
425            $($fields:tt)*
426        }
427    ) => {
428        $crate::__led_strip_collect_fields!{
429            name = $name,
430            pin = [],
431            len = [],
432            max_current = [],
433            engine = [],
434            gamma = [],
435            max_frames = [],
436            reset_us = [],
437            fields = [$($fields)*],
438        }
439    };
440    (
441        $vis:vis $name:ident {
442            $($fields:tt)*
443        }
444    ) => {
445        $crate::__paste! {
446            $crate::__led_strip_entry! {
447                [<__ $name _visibility_inner>] {
448                    $($fields)*
449                }
450            }
451            $vis type $name = [<__ $name _visibility_inner>];
452        }
453    };
454}
455
456#[cfg(target_os = "none")]
457#[doc(inline)]
458pub use led_strip;
459
460#[doc(hidden)]
461#[macro_export]
462macro_rules! __led_strip_collect_fields {
463    (
464        name = $name:ident,
465        pin = [$pin:ident],
466        len = [$len:expr],
467        max_current = [$($max_current:expr)?],
468        engine = [$($engine:tt)?],
469        gamma = [$($gamma:expr)?],
470        max_frames = [$($max_frames:expr)?],
471        reset_us = [$($reset_us:expr)?],
472        fields = [],
473    ) => {
474        $crate::__led_strip_dispatch_engine!(
475            $name,
476            $pin,
477            $len,
478            $crate::__led_strip_max_current_or_default!([$($max_current)?]),
479            [$($engine)?],
480            [$($gamma)?],
481            [$($max_frames)?],
482            [$($reset_us)?],
483        );
484    };
485    (
486        name = $name:ident,
487        pin = [],
488        len = [$($len:expr)?],
489        max_current = [$($max_current:expr)?],
490        engine = [$($engine:tt)?],
491        gamma = [$($gamma:expr)?],
492        max_frames = [$($max_frames:expr)?],
493        reset_us = [$($reset_us:expr)?],
494        fields = [],
495    ) => {
496        compile_error!("led_strip! missing required `pin` field");
497    };
498    (
499        name = $name:ident,
500        pin = [$pin:ident],
501        len = [],
502        max_current = [$($max_current:expr)?],
503        engine = [$($engine:tt)?],
504        gamma = [$($gamma:expr)?],
505        max_frames = [$($max_frames:expr)?],
506        reset_us = [$($reset_us:expr)?],
507        fields = [],
508    ) => {
509        compile_error!("led_strip! missing required `len` field");
510    };
511    (
512        name = $name:ident,
513        pin = [],
514        len = [$($len:expr)?],
515        max_current = [$($max_current:expr)?],
516        engine = [$($engine:tt)?],
517        gamma = [$($gamma:expr)?],
518        max_frames = [$($max_frames:expr)?],
519        reset_us = [$($reset_us:expr)?],
520        fields = [pin: $pin:ident $(, $($rest:tt)*)?],
521    ) => {
522        $crate::__led_strip_collect_fields!{
523            name = $name,
524            pin = [$pin],
525            len = [$($len)?],
526            max_current = [$($max_current)?],
527            engine = [$($engine)?],
528            gamma = [$($gamma)?],
529            max_frames = [$($max_frames)?],
530            reset_us = [$($reset_us)?],
531            fields = [$($($rest)*)?],
532        }
533    };
534    (
535        name = $name:ident,
536        pin = [$already_pin:ident],
537        len = [$($len:expr)?],
538        max_current = [$($max_current:expr)?],
539        engine = [$($engine:tt)?],
540        gamma = [$($gamma:expr)?],
541        max_frames = [$($max_frames:expr)?],
542        reset_us = [$($reset_us:expr)?],
543        fields = [pin: $pin:ident $(, $($rest:tt)*)?],
544    ) => {
545        compile_error!("led_strip! duplicate `pin` field");
546    };
547    (
548        name = $name:ident,
549        pin = [$($pin:ident)?],
550        len = [],
551        max_current = [$($max_current:expr)?],
552        engine = [$($engine:tt)?],
553        gamma = [$($gamma:expr)?],
554        max_frames = [$($max_frames:expr)?],
555        reset_us = [$($reset_us:expr)?],
556        fields = [len: $len:expr $(, $($rest:tt)*)?],
557    ) => {
558        $crate::__led_strip_collect_fields!{
559            name = $name,
560            pin = [$($pin)?],
561            len = [$len],
562            max_current = [$($max_current)?],
563            engine = [$($engine)?],
564            gamma = [$($gamma)?],
565            max_frames = [$($max_frames)?],
566            reset_us = [$($reset_us)?],
567            fields = [$($($rest)*)?],
568        }
569    };
570    (
571        name = $name:ident,
572        pin = [$($pin:ident)?],
573        len = [$already_len:expr],
574        max_current = [$($max_current:expr)?],
575        engine = [$($engine:tt)?],
576        gamma = [$($gamma:expr)?],
577        max_frames = [$($max_frames:expr)?],
578        reset_us = [$($reset_us:expr)?],
579        fields = [len: $len:expr $(, $($rest:tt)*)?],
580    ) => {
581        compile_error!("led_strip! duplicate `len` field");
582    };
583    (
584        name = $name:ident,
585        pin = [$($pin:ident)?],
586        len = [$($len:expr)?],
587        max_current = [],
588        engine = [$($engine:tt)?],
589        gamma = [$($gamma:expr)?],
590        max_frames = [$($max_frames:expr)?],
591        reset_us = [$($reset_us:expr)?],
592        fields = [max_current: $max_current:expr $(, $($rest:tt)*)?],
593    ) => {
594        $crate::__led_strip_collect_fields!{
595            name = $name,
596            pin = [$($pin)?],
597            len = [$($len)?],
598            max_current = [$max_current],
599            engine = [$($engine)?],
600            gamma = [$($gamma)?],
601            max_frames = [$($max_frames)?],
602            reset_us = [$($reset_us)?],
603            fields = [$($($rest)*)?],
604        }
605    };
606    (
607        name = $name:ident,
608        pin = [$($pin:ident)?],
609        len = [$($len:expr)?],
610        max_current = [$already_max_current:expr],
611        engine = [$($engine:tt)?],
612        gamma = [$($gamma:expr)?],
613        max_frames = [$($max_frames:expr)?],
614        reset_us = [$($reset_us:expr)?],
615        fields = [max_current: $max_current:expr $(, $($rest:tt)*)?],
616    ) => {
617        compile_error!("led_strip! duplicate `max_current` field");
618    };
619    (
620        name = $name:ident,
621        pin = [$($pin:ident)?],
622        len = [$($len:expr)?],
623        max_current = [$($max_current:expr)?],
624        engine = [],
625        gamma = [$($gamma:expr)?],
626        max_frames = [$($max_frames:expr)?],
627        reset_us = [$($reset_us:expr)?],
628        fields = [engine: Engine::Spi $(, $($rest:tt)*)?],
629    ) => {
630        $crate::__led_strip_collect_fields!{
631            name = $name,
632            pin = [$($pin)?],
633            len = [$($len)?],
634            max_current = [$($max_current)?],
635            engine = [Spi],
636            gamma = [$($gamma)?],
637            max_frames = [$($max_frames)?],
638            reset_us = [$($reset_us)?],
639            fields = [$($($rest)*)?],
640        }
641    };
642    (
643        name = $name:ident,
644        pin = [$($pin:ident)?],
645        len = [$($len:expr)?],
646        max_current = [$($max_current:expr)?],
647        engine = [],
648        gamma = [$($gamma:expr)?],
649        max_frames = [$($max_frames:expr)?],
650        reset_us = [$($reset_us:expr)?],
651        fields = [engine: $crate::led_strip::Engine::Spi $(, $($rest:tt)*)?],
652    ) => {
653        $crate::__led_strip_collect_fields!{
654            name = $name,
655            pin = [$($pin)?],
656            len = [$($len)?],
657            max_current = [$($max_current)?],
658            engine = [Spi],
659            gamma = [$($gamma)?],
660            max_frames = [$($max_frames)?],
661            reset_us = [$($reset_us)?],
662            fields = [$($($rest)*)?],
663        }
664    };
665    (
666        name = $name:ident,
667        pin = [$($pin:ident)?],
668        len = [$($len:expr)?],
669        max_current = [$($max_current:expr)?],
670        engine = [],
671        gamma = [$($gamma:expr)?],
672        max_frames = [$($max_frames:expr)?],
673        reset_us = [$($reset_us:expr)?],
674        fields = [engine: device_envoy_esp::led_strip::Engine::Spi $(, $($rest:tt)*)?],
675    ) => {
676        $crate::__led_strip_collect_fields!{
677            name = $name,
678            pin = [$($pin)?],
679            len = [$($len)?],
680            max_current = [$($max_current)?],
681            engine = [Spi],
682            gamma = [$($gamma)?],
683            max_frames = [$($max_frames)?],
684            reset_us = [$($reset_us)?],
685            fields = [$($($rest)*)?],
686        }
687    };
688    (
689        name = $name:ident,
690        pin = [$($pin:ident)?],
691        len = [$($len:expr)?],
692        max_current = [$($max_current:expr)?],
693        engine = [],
694        gamma = [$($gamma:expr)?],
695        max_frames = [$($max_frames:expr)?],
696        reset_us = [$($reset_us:expr)?],
697        fields = [engine: Engine::Rmt $(, $($rest:tt)*)?],
698    ) => {
699        $crate::__led_strip_collect_fields!{
700            name = $name,
701            pin = [$($pin)?],
702            len = [$($len)?],
703            max_current = [$($max_current)?],
704            engine = [Rmt],
705            gamma = [$($gamma)?],
706            max_frames = [$($max_frames)?],
707            reset_us = [$($reset_us)?],
708            fields = [$($($rest)*)?],
709        }
710    };
711    (
712        name = $name:ident,
713        pin = [$($pin:ident)?],
714        len = [$($len:expr)?],
715        max_current = [$($max_current:expr)?],
716        engine = [],
717        gamma = [$($gamma:expr)?],
718        max_frames = [$($max_frames:expr)?],
719        reset_us = [$($reset_us:expr)?],
720        fields = [engine: $crate::led_strip::Engine::Rmt $(, $($rest:tt)*)?],
721    ) => {
722        $crate::__led_strip_collect_fields!{
723            name = $name,
724            pin = [$($pin)?],
725            len = [$($len)?],
726            max_current = [$($max_current)?],
727            engine = [Rmt],
728            gamma = [$($gamma)?],
729            max_frames = [$($max_frames)?],
730            reset_us = [$($reset_us)?],
731            fields = [$($($rest)*)?],
732        }
733    };
734    (
735        name = $name:ident,
736        pin = [$($pin:ident)?],
737        len = [$($len:expr)?],
738        max_current = [$($max_current:expr)?],
739        engine = [],
740        gamma = [$($gamma:expr)?],
741        max_frames = [$($max_frames:expr)?],
742        reset_us = [$($reset_us:expr)?],
743        fields = [engine: device_envoy_esp::led_strip::Engine::Rmt $(, $($rest:tt)*)?],
744    ) => {
745        $crate::__led_strip_collect_fields!{
746            name = $name,
747            pin = [$($pin)?],
748            len = [$($len)?],
749            max_current = [$($max_current)?],
750            engine = [Rmt],
751            gamma = [$($gamma)?],
752            max_frames = [$($max_frames)?],
753            reset_us = [$($reset_us)?],
754            fields = [$($($rest)*)?],
755        }
756    };
757    (
758        name = $name:ident,
759        pin = [$($pin:ident)?],
760        len = [$($len:expr)?],
761        max_current = [$($max_current:expr)?],
762        engine = [$already_engine:tt],
763        gamma = [$($gamma:expr)?],
764        max_frames = [$($max_frames:expr)?],
765        reset_us = [$($reset_us:expr)?],
766        fields = [engine: $ignored:path $(, $($rest:tt)*)?],
767    ) => {
768        compile_error!("led_strip! duplicate `engine` field");
769    };
770    (
771        name = $name:ident,
772        pin = [$($pin:ident)?],
773        len = [$($len:expr)?],
774        max_current = [$($max_current:expr)?],
775        engine = [],
776        gamma = [$($gamma:expr)?],
777        max_frames = [$($max_frames:expr)?],
778        reset_us = [$($reset_us:expr)?],
779        fields = [engine: $ignored:path $(, $($rest:tt)*)?],
780    ) => {
781        compile_error!("led_strip! engine must be Engine::Rmt or Engine::Spi");
782    };
783    (
784        name = $name:ident,
785        pin = [$($pin:ident)?],
786        len = [$($len:expr)?],
787        max_current = [$($max_current:expr)?],
788        engine = [$($engine:tt)?],
789        gamma = [],
790        max_frames = [$($max_frames:expr)?],
791        reset_us = [$($reset_us:expr)?],
792        fields = [gamma: $gamma:expr $(, $($rest:tt)*)?],
793    ) => {
794        $crate::__led_strip_collect_fields!{
795            name = $name,
796            pin = [$($pin)?],
797            len = [$($len)?],
798            max_current = [$($max_current)?],
799            engine = [$($engine)?],
800            gamma = [$gamma],
801            max_frames = [$($max_frames)?],
802            reset_us = [$($reset_us)?],
803            fields = [$($($rest)*)?],
804        }
805    };
806    (
807        name = $name:ident,
808        pin = [$($pin:ident)?],
809        len = [$($len:expr)?],
810        max_current = [$($max_current:expr)?],
811        engine = [$($engine:tt)?],
812        gamma = [$already_gamma:expr],
813        max_frames = [$($max_frames:expr)?],
814        reset_us = [$($reset_us:expr)?],
815        fields = [gamma: $gamma:expr $(, $($rest:tt)*)?],
816    ) => {
817        compile_error!("led_strip! duplicate `gamma` field");
818    };
819    (
820        name = $name:ident,
821        pin = [$($pin:ident)?],
822        len = [$($len:expr)?],
823        max_current = [$($max_current:expr)?],
824        engine = [$($engine:tt)?],
825        gamma = [$($gamma:expr)?],
826        max_frames = [],
827        reset_us = [$($reset_us:expr)?],
828        fields = [max_frames: $max_frames:expr $(, $($rest:tt)*)?],
829    ) => {
830        $crate::__led_strip_collect_fields!{
831            name = $name,
832            pin = [$($pin)?],
833            len = [$($len)?],
834            max_current = [$($max_current)?],
835            engine = [$($engine)?],
836            gamma = [$($gamma)?],
837            max_frames = [$max_frames],
838            reset_us = [$($reset_us)?],
839            fields = [$($($rest)*)?],
840        }
841    };
842    (
843        name = $name:ident,
844        pin = [$($pin:ident)?],
845        len = [$($len:expr)?],
846        max_current = [$($max_current:expr)?],
847        engine = [$($engine:tt)?],
848        gamma = [$($gamma:expr)?],
849        max_frames = [$already_max_frames:expr],
850        reset_us = [$($reset_us:expr)?],
851        fields = [max_frames: $max_frames:expr $(, $($rest:tt)*)?],
852    ) => {
853        compile_error!("led_strip! duplicate `max_frames` field");
854    };
855    (
856        name = $name:ident,
857        pin = [$($pin:ident)?],
858        len = [$($len:expr)?],
859        max_current = [$($max_current:expr)?],
860        engine = [$($engine:tt)?],
861        gamma = [$($gamma:expr)?],
862        max_frames = [$($max_frames:expr)?],
863        reset_us = [],
864        fields = [reset_us: $reset_us:expr $(, $($rest:tt)*)?],
865    ) => {
866        $crate::__led_strip_collect_fields!{
867            name = $name,
868            pin = [$($pin)?],
869            len = [$($len)?],
870            max_current = [$($max_current)?],
871            engine = [$($engine)?],
872            gamma = [$($gamma)?],
873            max_frames = [$($max_frames)?],
874            reset_us = [$reset_us],
875            fields = [$($($rest)*)?],
876        }
877    };
878    (
879        name = $name:ident,
880        pin = [$($pin:ident)?],
881        len = [$($len:expr)?],
882        max_current = [$($max_current:expr)?],
883        engine = [$($engine:tt)?],
884        gamma = [$($gamma:expr)?],
885        max_frames = [$($max_frames:expr)?],
886        reset_us = [$already_reset_us:expr],
887        fields = [reset_us: $reset_us:expr $(, $($rest:tt)*)?],
888    ) => {
889        compile_error!("led_strip! duplicate `reset_us` field");
890    };
891    (
892        name = $name:ident,
893        pin = [$($pin:ident)?],
894        len = [$($len:expr)?],
895        max_current = [$($max_current:expr)?],
896        engine = [$($engine:tt)?],
897        gamma = [$($gamma:expr)?],
898        max_frames = [$($max_frames:expr)?],
899        reset_us = [$($reset_us:expr)?],
900        fields = [$field:ident : $value:expr $(, $($rest:tt)*)?],
901    ) => {
902        compile_error!(
903            "led_strip! unknown field; expected `pin`, `len`, `max_current`, `engine`, `gamma`, `max_frames`, or `reset_us`"
904        );
905    };
906}
907
908#[doc(hidden)]
909#[macro_export]
910macro_rules! __led_strip_max_current_or_default {
911    ([$max_current:expr]) => {
912        $max_current
913    };
914    ([]) => {
915        $crate::led_strip::CURRENT_DEFAULT
916    };
917}
918
919/// Internal helper macro used by [`led_strip!`]. Do not call directly.
920///
921/// This is `pub` because the macro expansion happens at the call site in
922/// downstream crates, so the token tree must be accessible from outside this
923/// crate.
924// Must be `pub` for macro expansion at foreign call site — not user-facing.
925#[doc(hidden)]
926#[macro_export]
927macro_rules! __led_strip_inner {
928    (
929        $name:ident,
930        $pin:ident,
931        $len:expr,
932        $max_current:expr,
933        [$($gamma:expr)?],
934        [$($max_frames:expr)?],
935        [$($led2d_layout:expr)?],
936        [$($led2d_font:expr)?],
937    ) => {
938        $crate::__led_strip_impl!{
939            name        = $name,
940            pin         = $pin,
941            len         = $len,
942            max_current = $max_current,
943            gamma       = $crate::__led_strip_first_or_default!(
944                              [$($gamma)?],
945                              $crate::led_strip::GAMMA_DEFAULT
946                          ),
947            max_frames  = $crate::__led_strip_first_or_default!(
948                              [$($max_frames)?],
949                              $crate::led_strip::MAX_FRAMES_DEFAULT
950                          ),
951            led2d_layout = [$($led2d_layout)?],
952            led2d_font = [$($led2d_font)?],
953        }
954    };
955}
956
957/// Parse optional led_strip! fields (`engine`, `gamma`, `max_frames`, `reset_us`) in any order.
958///
959/// This is `pub` for downstream macro expansion at call sites.
960#[doc(hidden)]
961#[macro_export]
962macro_rules! __led_strip_parse_options {
963    (
964        name = $name:ident,
965        pin = $pin:ident,
966        len = $len:expr,
967        max_current = $max_current:expr,
968        engine = [$($engine:tt)*],
969        gamma = [$($gamma:expr)?],
970        max_frames = [$($max_frames:expr)?],
971        reset_us = [$($reset_us:expr)?],
972    ) => {
973        $crate::__led_strip_dispatch_engine! {
974            $name,
975            $pin,
976            $len,
977            $max_current,
978            [$($engine)*],
979            [$($gamma)?],
980            [$($max_frames)?],
981            [$($reset_us)?],
982        }
983    };
984    (
985        name = $name:ident,
986        pin = $pin:ident,
987        len = $len:expr,
988        max_current = $max_current:expr,
989        engine = [],
990        gamma = [$($gamma:expr)?],
991        max_frames = [$($max_frames:expr)?],
992        reset_us = [$($reset_us:expr)?],
993        engine: Engine::Spi
994        $(, $($tail:tt)*)?
995    ) => {
996        $crate::__led_strip_parse_options! {
997            name = $name,
998            pin = $pin,
999            len = $len,
1000            max_current = $max_current,
1001            engine = [Spi],
1002            gamma = [$($gamma)?],
1003            max_frames = [$($max_frames)?],
1004            reset_us = [$($reset_us)?],
1005            $($($tail)*)?
1006        }
1007    };
1008    (
1009        name = $name:ident,
1010        pin = $pin:ident,
1011        len = $len:expr,
1012        max_current = $max_current:expr,
1013        engine = [],
1014        gamma = [$($gamma:expr)?],
1015        max_frames = [$($max_frames:expr)?],
1016        reset_us = [$($reset_us:expr)?],
1017        engine: $crate::led_strip::Engine::Spi
1018        $(, $($tail:tt)*)?
1019    ) => {
1020        $crate::__led_strip_parse_options! {
1021            name = $name,
1022            pin = $pin,
1023            len = $len,
1024            max_current = $max_current,
1025            engine = [Spi],
1026            gamma = [$($gamma)?],
1027            max_frames = [$($max_frames)?],
1028            reset_us = [$($reset_us)?],
1029            $($($tail)*)?
1030        }
1031    };
1032    (
1033        name = $name:ident,
1034        pin = $pin:ident,
1035        len = $len:expr,
1036        max_current = $max_current:expr,
1037        engine = [],
1038        gamma = [$($gamma:expr)?],
1039        max_frames = [$($max_frames:expr)?],
1040        reset_us = [$($reset_us:expr)?],
1041        engine: device_envoy_esp::led_strip::Engine::Spi
1042        $(, $($tail:tt)*)?
1043    ) => {
1044        $crate::__led_strip_parse_options! {
1045            name = $name,
1046            pin = $pin,
1047            len = $len,
1048            max_current = $max_current,
1049            engine = [Spi],
1050            gamma = [$($gamma)?],
1051            max_frames = [$($max_frames)?],
1052            reset_us = [$($reset_us)?],
1053            $($($tail)*)?
1054        }
1055    };
1056    (
1057        name = $name:ident,
1058        pin = $pin:ident,
1059        len = $len:expr,
1060        max_current = $max_current:expr,
1061        engine = [],
1062        gamma = [$($gamma:expr)?],
1063        max_frames = [$($max_frames:expr)?],
1064        reset_us = [$($reset_us:expr)?],
1065        engine: Engine::Rmt
1066        $(, $($tail:tt)*)?
1067    ) => {
1068        $crate::__led_strip_parse_options! {
1069            name = $name,
1070            pin = $pin,
1071            len = $len,
1072            max_current = $max_current,
1073            engine = [Rmt],
1074            gamma = [$($gamma)?],
1075            max_frames = [$($max_frames)?],
1076            reset_us = [$($reset_us)?],
1077            $($($tail)*)?
1078        }
1079    };
1080    (
1081        name = $name:ident,
1082        pin = $pin:ident,
1083        len = $len:expr,
1084        max_current = $max_current:expr,
1085        engine = [],
1086        gamma = [$($gamma:expr)?],
1087        max_frames = [$($max_frames:expr)?],
1088        reset_us = [$($reset_us:expr)?],
1089        engine: $crate::led_strip::Engine::Rmt
1090        $(, $($tail:tt)*)?
1091    ) => {
1092        $crate::__led_strip_parse_options! {
1093            name = $name,
1094            pin = $pin,
1095            len = $len,
1096            max_current = $max_current,
1097            engine = [Rmt],
1098            gamma = [$($gamma)?],
1099            max_frames = [$($max_frames)?],
1100            reset_us = [$($reset_us)?],
1101            $($($tail)*)?
1102        }
1103    };
1104    (
1105        name = $name:ident,
1106        pin = $pin:ident,
1107        len = $len:expr,
1108        max_current = $max_current:expr,
1109        engine = [],
1110        gamma = [$($gamma:expr)?],
1111        max_frames = [$($max_frames:expr)?],
1112        reset_us = [$($reset_us:expr)?],
1113        engine: device_envoy_esp::led_strip::Engine::Rmt
1114        $(, $($tail:tt)*)?
1115    ) => {
1116        $crate::__led_strip_parse_options! {
1117            name = $name,
1118            pin = $pin,
1119            len = $len,
1120            max_current = $max_current,
1121            engine = [Rmt],
1122            gamma = [$($gamma)?],
1123            max_frames = [$($max_frames)?],
1124            reset_us = [$($reset_us)?],
1125            $($($tail)*)?
1126        }
1127    };
1128    (
1129        name = $name:ident,
1130        pin = $pin:ident,
1131        len = $len:expr,
1132        max_current = $max_current:expr,
1133        engine = [$($engine:tt)+],
1134        gamma = [$($gamma:expr)?],
1135        max_frames = [$($max_frames:expr)?],
1136        reset_us = [$($reset_us:expr)?],
1137        engine: $ignored:path
1138        $(, $($tail:tt)*)?
1139    ) => {
1140        compile_error!("led_strip! duplicate `engine` field");
1141    };
1142    (
1143        name = $name:ident,
1144        pin = $pin:ident,
1145        len = $len:expr,
1146        max_current = $max_current:expr,
1147        engine = [],
1148        gamma = [$($gamma:expr)?],
1149        max_frames = [$($max_frames:expr)?],
1150        reset_us = [$($reset_us:expr)?],
1151        engine: $ignored:path
1152        $(, $($tail:tt)*)?
1153    ) => {
1154        compile_error!("led_strip! engine must be Engine::Rmt or Engine::Spi");
1155    };
1156    (
1157        name = $name:ident,
1158        pin = $pin:ident,
1159        len = $len:expr,
1160        max_current = $max_current:expr,
1161        engine = [$($engine:tt)*],
1162        gamma = [],
1163        max_frames = [$($max_frames:expr)?],
1164        reset_us = [$($reset_us:expr)?],
1165        gamma: $gamma:expr
1166        $(, $($tail:tt)*)?
1167    ) => {
1168        $crate::__led_strip_parse_options! {
1169            name = $name,
1170            pin = $pin,
1171            len = $len,
1172            max_current = $max_current,
1173            engine = [$($engine)*],
1174            gamma = [$gamma],
1175            max_frames = [$($max_frames)?],
1176            reset_us = [$($reset_us)?],
1177            $($($tail)*)?
1178        }
1179    };
1180    (
1181        name = $name:ident,
1182        pin = $pin:ident,
1183        len = $len:expr,
1184        max_current = $max_current:expr,
1185        engine = [$($engine:tt)*],
1186        gamma = [$already_gamma:expr],
1187        max_frames = [$($max_frames:expr)?],
1188        reset_us = [$($reset_us:expr)?],
1189        gamma: $gamma:expr
1190        $(, $($tail:tt)*)?
1191    ) => {
1192        compile_error!("led_strip! duplicate `gamma` field");
1193    };
1194    (
1195        name = $name:ident,
1196        pin = $pin:ident,
1197        len = $len:expr,
1198        max_current = $max_current:expr,
1199        engine = [$($engine:tt)*],
1200        gamma = [$($gamma:expr)?],
1201        max_frames = [],
1202        reset_us = [$($reset_us:expr)?],
1203        max_frames: $max_frames:expr
1204        $(, $($tail:tt)*)?
1205    ) => {
1206        $crate::__led_strip_parse_options! {
1207            name = $name,
1208            pin = $pin,
1209            len = $len,
1210            max_current = $max_current,
1211            engine = [$($engine)*],
1212            gamma = [$($gamma)?],
1213            max_frames = [$max_frames],
1214            reset_us = [$($reset_us)?],
1215            $($($tail)*)?
1216        }
1217    };
1218    (
1219        name = $name:ident,
1220        pin = $pin:ident,
1221        len = $len:expr,
1222        max_current = $max_current:expr,
1223        engine = [$($engine:tt)*],
1224        gamma = [$($gamma:expr)?],
1225        max_frames = [$already_max_frames:expr],
1226        reset_us = [$($reset_us:expr)?],
1227        max_frames: $max_frames:expr
1228        $(, $($tail:tt)*)?
1229    ) => {
1230        compile_error!("led_strip! duplicate `max_frames` field");
1231    };
1232    (
1233        name = $name:ident,
1234        pin = $pin:ident,
1235        len = $len:expr,
1236        max_current = $max_current:expr,
1237        engine = [$($engine:tt)*],
1238        gamma = [$($gamma:expr)?],
1239        max_frames = [$($max_frames:expr)?],
1240        reset_us = [],
1241        reset_us: $reset_us:expr
1242        $(, $($tail:tt)*)?
1243    ) => {
1244        $crate::__led_strip_parse_options! {
1245            name = $name,
1246            pin = $pin,
1247            len = $len,
1248            max_current = $max_current,
1249            engine = [$($engine)*],
1250            gamma = [$($gamma)?],
1251            max_frames = [$($max_frames)?],
1252            reset_us = [$reset_us],
1253            $($($tail)*)?
1254        }
1255    };
1256    (
1257        name = $name:ident,
1258        pin = $pin:ident,
1259        len = $len:expr,
1260        max_current = $max_current:expr,
1261        engine = [$($engine:tt)*],
1262        gamma = [$($gamma:expr)?],
1263        max_frames = [$($max_frames:expr)?],
1264        reset_us = [$already_reset_us:expr],
1265        reset_us: $reset_us:expr
1266        $(, $($tail:tt)*)?
1267    ) => {
1268        compile_error!("led_strip! duplicate `reset_us` field");
1269    };
1270    (
1271        name = $name:ident,
1272        pin = $pin:ident,
1273        len = $len:expr,
1274        max_current = $max_current:expr,
1275        engine = [$($engine:tt)*],
1276        gamma = [$($gamma:expr)?],
1277        max_frames = [$($max_frames:expr)?],
1278        reset_us = [$($reset_us:expr)?],
1279        $field:ident : $value:expr
1280        $(, $($tail:tt)*)?
1281    ) => {
1282        compile_error!("led_strip! unknown field; expected `engine`, `gamma`, `max_frames`, or `reset_us`");
1283    };
1284}
1285
1286/// Dispatch parsed led_strip! options to RMT or SPI backend.
1287///
1288/// This is `pub` for downstream macro expansion at call sites.
1289#[doc(hidden)]
1290#[macro_export]
1291macro_rules! __led_strip_dispatch_engine {
1292    (
1293        $name:ident,
1294        $pin:ident,
1295        $len:expr,
1296        $max_current:expr,
1297        [Spi],
1298        [$($gamma:expr)?],
1299        [$($max_frames:expr)?],
1300        [$($reset_us:expr)?],
1301    ) => {
1302        $crate::led_strip::spi::__led_strip_spi_inner!{
1303            $name,
1304            $pin,
1305            $len,
1306            $max_current,
1307            [$($gamma)?],
1308            [$($max_frames)?],
1309            [$($reset_us)?],
1310            [],
1311            [],
1312        }
1313    };
1314    (
1315        $name:ident,
1316        $pin:ident,
1317        $len:expr,
1318        $max_current:expr,
1319        [Rmt],
1320        [$($gamma:expr)?],
1321        [$($max_frames:expr)?],
1322        [$reset_us:expr],
1323    ) => {
1324        compile_error!("led_strip! `reset_us` is only supported with `engine: Engine::Spi`");
1325    };
1326    (
1327        $name:ident,
1328        $pin:ident,
1329        $len:expr,
1330        $max_current:expr,
1331        [Rmt],
1332        [$($gamma:expr)?],
1333        [$($max_frames:expr)?],
1334        [],
1335    ) => {
1336        $crate::__led_strip_dispatch_rmt_engine!{
1337            $name,
1338            $pin,
1339            $len,
1340            $max_current,
1341            [$($gamma)?],
1342            [$($max_frames)?],
1343        }
1344    };
1345    (
1346        $name:ident,
1347        $pin:ident,
1348        $len:expr,
1349        $max_current:expr,
1350        [],
1351        [$($gamma:expr)?],
1352        [$($max_frames:expr)?],
1353        [$reset_us:expr],
1354    ) => {
1355        compile_error!("led_strip! `reset_us` is only supported with `engine: Engine::Spi`");
1356    };
1357    (
1358        $name:ident,
1359        $pin:ident,
1360        $len:expr,
1361        $max_current:expr,
1362        [],
1363        [$($gamma:expr)?],
1364        [$($max_frames:expr)?],
1365        [],
1366    ) => {
1367        $crate::__led_strip_dispatch_default_engine!{
1368            $name,
1369            $pin,
1370            $len,
1371            $max_current,
1372            [$($gamma)?],
1373            [$($max_frames)?],
1374        }
1375    };
1376}
1377
1378/// Internal helper used by `led_strip!` to dispatch explicit `Engine::Rmt`.
1379#[doc(hidden)]
1380#[cfg(esp_has_rmt)]
1381#[macro_export]
1382macro_rules! __led_strip_dispatch_rmt_engine {
1383    (
1384        $name:ident,
1385        $pin:ident,
1386        $len:expr,
1387        $max_current:expr,
1388        [$($gamma:expr)?],
1389        [$($max_frames:expr)?],
1390    ) => {
1391        $crate::led_strip::__led_strip_inner!{
1392            $name,
1393            $pin,
1394            $len,
1395            $max_current,
1396            [$($gamma)?],
1397            [$($max_frames)?],
1398            [],
1399            [],
1400        }
1401    };
1402}
1403
1404/// Internal helper used by `led_strip!` to dispatch explicit `Engine::Rmt`.
1405#[doc(hidden)]
1406#[cfg(not(esp_has_rmt))]
1407#[macro_export]
1408macro_rules! __led_strip_dispatch_rmt_engine {
1409    (
1410        $name:ident,
1411        $pin:ident,
1412        $len:expr,
1413        $max_current:expr,
1414        [$($gamma:expr)?],
1415        [$($max_frames:expr)?],
1416    ) => {
1417        compile_error!(
1418            "led_strip! `engine: Engine::Rmt` requires an RMT-capable chip; use `engine: Engine::Spi`."
1419        );
1420    };
1421}
1422
1423/// Internal helper used by `led_strip!` to dispatch the default engine.
1424#[doc(hidden)]
1425#[cfg(esp_has_rmt)]
1426#[macro_export]
1427macro_rules! __led_strip_dispatch_default_engine {
1428    (
1429        $name:ident,
1430        $pin:ident,
1431        $len:expr,
1432        $max_current:expr,
1433        [$($gamma:expr)?],
1434        [$($max_frames:expr)?],
1435    ) => {
1436        $crate::__led_strip_dispatch_rmt_engine!{
1437            $name,
1438            $pin,
1439            $len,
1440            $max_current,
1441            [$($gamma)?],
1442            [$($max_frames)?],
1443        }
1444    };
1445}
1446
1447/// Internal helper used by `led_strip!` to dispatch the default engine.
1448#[doc(hidden)]
1449#[cfg(not(esp_has_rmt))]
1450#[macro_export]
1451macro_rules! __led_strip_dispatch_default_engine {
1452    (
1453        $name:ident,
1454        $pin:ident,
1455        $len:expr,
1456        $max_current:expr,
1457        [$($gamma:expr)?],
1458        [$($max_frames:expr)?],
1459    ) => {
1460        $crate::led_strip::spi::__led_strip_spi_inner!{
1461            $name,
1462            $pin,
1463            $len,
1464            $max_current,
1465            [$($gamma)?],
1466            [$($max_frames)?],
1467            [],
1468            [],
1469            [],
1470        }
1471    };
1472}
1473
1474/// Pick the first element of a bracketed list, or fall back to a default.
1475/// Only for use in `led_strip!` expansion. Do not call directly.
1476// Must be `pub` for macro expansion at foreign call site — not user-facing.
1477#[doc(hidden)]
1478#[macro_export]
1479macro_rules! __led_strip_first_or_default {
1480    ([$value:expr], $_default:expr) => {
1481        $value
1482    };
1483    ([],             $default:expr) => {
1484        $default
1485    };
1486}
1487
1488/// Emit optional 2D-panel constants and methods on a generated strip type.
1489#[doc(hidden)]
1490#[macro_export]
1491macro_rules! __led2d_strip_methods {
1492    ($_leds:expr, $max_frames:expr, [$led_layout:expr], [$font:expr]) => {
1493        /// Default font used by text helpers.
1494        pub const FONT: $crate::led2d::Led2dFont = $font;
1495        /// Panel width in pixels.
1496        pub const WIDTH: usize = $led_layout.width();
1497        /// Panel height in pixels.
1498        pub const HEIGHT: usize = $led_layout.height();
1499        /// Panel dimensions.
1500        pub const SIZE: $crate::led2d::Size =
1501            $crate::led2d::Frame2d::<{ $led_layout.width() }, { $led_layout.height() }>::SIZE;
1502        /// Top-left corner coordinate.
1503        pub const TOP_LEFT: $crate::led2d::Point =
1504            $crate::led2d::Frame2d::<{ $led_layout.width() }, { $led_layout.height() }>::TOP_LEFT;
1505        /// Top-right corner coordinate.
1506        pub const TOP_RIGHT: $crate::led2d::Point =
1507            $crate::led2d::Frame2d::<{ $led_layout.width() }, { $led_layout.height() }>::TOP_RIGHT;
1508        /// Bottom-left corner coordinate.
1509        pub const BOTTOM_LEFT: $crate::led2d::Point = $crate::led2d::Frame2d::<
1510            { $led_layout.width() },
1511            { $led_layout.height() },
1512        >::BOTTOM_LEFT;
1513        /// Bottom-right corner coordinate.
1514        pub const BOTTOM_RIGHT: $crate::led2d::Point = $crate::led2d::Frame2d::<
1515            { $led_layout.width() },
1516            { $led_layout.height() },
1517        >::BOTTOM_RIGHT;
1518    };
1519    ($_leds:expr, $_max_frames:expr, [], []) => {};
1520}
1521
1522/// Emit optional LED2D trait impl for generated strip type.
1523#[doc(hidden)]
1524#[macro_export]
1525macro_rules! __led2d_strip_trait_impl {
1526    ($name:ident, [$led_layout:expr], [$font:expr], $max_frames:expr) => {
1527        impl $crate::led2d::Led2d<{ $led_layout.width() }, { $led_layout.height() }>
1528            for &'static $name
1529        {
1530            const WIDTH: usize = $name::WIDTH;
1531            const HEIGHT: usize = $name::HEIGHT;
1532            const LEN: usize = $name::LEN;
1533            const SIZE: $crate::led2d::Size = $name::SIZE;
1534            const TOP_LEFT: $crate::led2d::Point = $name::TOP_LEFT;
1535            const TOP_RIGHT: $crate::led2d::Point = $name::TOP_RIGHT;
1536            const BOTTOM_LEFT: $crate::led2d::Point = $name::BOTTOM_LEFT;
1537            const BOTTOM_RIGHT: $crate::led2d::Point = $name::BOTTOM_RIGHT;
1538            const MAX_FRAMES: usize = $max_frames;
1539            const MAX_BRIGHTNESS: u8 = $name::MAX_BRIGHTNESS;
1540            const FONT: $crate::led2d::Led2dFont = $font;
1541
1542            fn write_frame(
1543                &self,
1544                frame2d: $crate::led2d::Frame2d<{ $led_layout.width() }, { $led_layout.height() }>,
1545            ) {
1546                let led2d = $crate::led2d::Led2dEsp::new(*self, &$led_layout);
1547                $crate::led2d::Led2dStripBacked::write_frame(&led2d, frame2d);
1548            }
1549
1550            fn animate<I>(&self, frames: I)
1551            where
1552                I: IntoIterator,
1553                I::Item: ::core::borrow::Borrow<(
1554                        $crate::led2d::Frame2d<{ $led_layout.width() }, { $led_layout.height() }>,
1555                        embassy_time::Duration,
1556                    )>,
1557            {
1558                let led2d = $crate::led2d::Led2dEsp::new(*self, &$led_layout);
1559                $crate::led2d::Led2dStripBacked::animate(&led2d, frames);
1560            }
1561        }
1562    };
1563    ($_name:ident, [], [], $_max_frames:expr) => {};
1564}
1565
1566/// Core implementation macro. Do not call directly.
1567// Must be `pub` for macro expansion at foreign call site — not user-facing.
1568#[doc(hidden)]
1569#[macro_export]
1570macro_rules! __led_strip_impl {
1571    (
1572        name        = $name:ident,
1573        pin         = $pin:ident,
1574        len         = $len:expr,
1575        max_current = $max_current:expr,
1576        gamma       = $gamma:expr,
1577        max_frames  = $max_frames:expr,
1578        led2d_layout = [$($led2d_layout:expr)?],
1579        led2d_font = [$($led2d_font:expr)?],
1580    ) => {
1581        ::paste::paste! {
1582            // ------------------------------------------------------------------
1583            // Module holding concrete const values for this strip instance.
1584            // Named after the struct in snake_case to avoid collisions.
1585            // ------------------------------------------------------------------
1586            mod [<$name:snake _consts>] {
1587                /// Number of LED pixels.
1588                pub const LEDS: usize = $len;
1589                /// Pulse buffer length: 24 bits per LED plus 1 end marker.
1590                pub const PULSES: usize = LEDS * 24 + 1;
1591                /// Maximum simultaneous-on current in milliamps at full brightness.
1592                pub const WORST_CASE_MA: u32 = LEDS as u32 * 60;
1593            }
1594
1595            // ------------------------------------------------------------------
1596            // Static resources (signals etc.) — hidden from public docs.
1597            // ------------------------------------------------------------------
1598            static [<$name:snake:upper _STATIC>]:
1599                $crate::led_strip::LedStripStatic<
1600                    { [<$name:snake _consts>]::LEDS },
1601                    { $max_frames },
1602                > = $crate::led_strip::LedStripEsp::new_static();
1603
1604            // ------------------------------------------------------------------
1605            // Public struct.
1606            // ------------------------------------------------------------------
1607            pub struct $name {
1608                inner: $crate::led_strip::LedStripEsp<
1609                    { [<$name:snake _consts>]::LEDS },
1610                    { $max_frames },
1611                >,
1612            }
1613
1614            impl $name {
1615                /// Number of pixels in this strip.
1616                pub const LEN: usize = [<$name:snake _consts>]::LEDS;
1617
1618                /// Maximum number of animation frames.
1619                pub const MAX_FRAMES: usize = $max_frames;
1620
1621                /// Maximum per-channel brightness (0–255) computed from
1622                /// `max_current`.
1623                pub const MAX_BRIGHTNESS: u8 = <$crate::led_strip::Current>::max_brightness(
1624                    $max_current,
1625                    [<$name:snake _consts>]::WORST_CASE_MA,
1626                );
1627
1628                /// Combined gamma + brightness lookup table (const, zero cost).
1629                pub const COMBO_TABLE: [u8; 256] =
1630                    $crate::led_strip::generate_combo_table($gamma, Self::MAX_BRIGHTNESS);
1631
1632                $crate::__led2d_strip_methods!(
1633                    { [<$name:snake _consts>]::LEDS },
1634                    { $max_frames },
1635                    [$($led2d_layout)?],
1636                    [$($led2d_font)?]
1637                );
1638
1639                /// Construct the strip controller from an owned TX channel creator and GPIO pin.
1640                ///
1641                /// This configures a TX channel from a shared `rmt80` hub using
1642                /// [`ws2812_tx_config`](crate::init_and_start::rmt::ws2812_tx_config).
1643                pub fn new(
1644                    pin: $crate::esp_hal::peripherals::$pin<'static>,
1645                    channel_creator: impl ::esp_hal::rmt::TxChannelCreator<
1646                        'static,
1647                        ::esp_hal::Blocking,
1648                    >,
1649                    spawner: ::embassy_executor::Spawner,
1650                ) -> $crate::Result<&'static Self> {
1651                    use ::static_cell::StaticCell;
1652
1653                    static INSTANCE: StaticCell<$name> = StaticCell::new();
1654                    static COMBO: StaticCell<[u8; 256]> = StaticCell::new();
1655
1656                    let combo_ref: &'static [u8; 256] =
1657                        COMBO.init(<$name>::COMBO_TABLE);
1658
1659                    let channel = channel_creator
1660                        .configure_tx(&$crate::init_and_start::rmt::ws2812_tx_config())
1661                        .map_err($crate::Error::RmtConfig)?
1662                        .with_pin(pin);
1663
1664                    let driver =
1665                        $crate::led_strip::RmtWs2812::<
1666                            { [<$name:snake _consts>]::LEDS },
1667                            { [<$name:snake _consts>]::PULSES },
1668                        >::new(channel);
1669
1670                    let strip_static: &'static _ = &[<$name:snake:upper _STATIC>];
1671
1672                    spawner
1673                        .spawn([<$name:snake _device_task>](driver, strip_static, combo_ref).map_err($crate::Error::TaskSpawn)?);
1674
1675                    let instance = INSTANCE.init($name {
1676                        inner: $crate::led_strip::LedStripEsp::new(strip_static),
1677                    });
1678                    Ok(instance)
1679                }
1680            }
1681
1682            impl $crate::led_strip::LedStrip<{ [<$name:snake _consts>]::LEDS }> for $name {
1683                const MAX_FRAMES: usize = $max_frames;
1684                const MAX_BRIGHTNESS: u8 = Self::MAX_BRIGHTNESS;
1685
1686                fn write_frame(
1687                    &self,
1688                    frame: $crate::led_strip::Frame1d<{ [<$name:snake _consts>]::LEDS }>,
1689                ) {
1690                    $crate::led_strip::__write_frame(self.inner.__command_signal(), frame);
1691                }
1692
1693                fn animate<I>(&self, frames: I)
1694                where
1695                    I: IntoIterator,
1696                    I::Item: ::core::borrow::Borrow<(
1697                        $crate::led_strip::Frame1d<{ [<$name:snake _consts>]::LEDS }>,
1698                        embassy_time::Duration,
1699                    )>,
1700                {
1701                    $crate::led_strip::__animate(self.inner.__command_signal(), frames);
1702                }
1703            }
1704
1705            $crate::__led2d_strip_trait_impl!(
1706                $name,
1707                [$($led2d_layout)?],
1708                [$($led2d_font)?],
1709                $max_frames
1710            );
1711
1712            // ------------------------------------------------------------------
1713            // Background task (embassy task function).
1714            // ------------------------------------------------------------------
1715            #[::embassy_executor::task]
1716            async fn [<$name:snake _device_task>](
1717                driver: $crate::led_strip::RmtWs2812<
1718                    'static,
1719                    { [<$name:snake _consts>]::LEDS },
1720                    { [<$name:snake _consts>]::PULSES },
1721                >,
1722                strip_static: &'static $crate::led_strip::LedStripStatic<
1723                    { [<$name:snake _consts>]::LEDS },
1724                    { $max_frames },
1725                >,
1726                combo_table: &'static [u8; 256],
1727            ) {
1728                $crate::led_strip::led_strip_device_loop(
1729                    driver,
1730                    strip_static.command_signal(),
1731                    combo_table,
1732                )
1733                .await;
1734            }
1735        }
1736    };
1737}
1738
1739// ============================================================================
1740// SPI sub-module
1741// ============================================================================
1742
1743#[cfg(target_os = "none")]
1744#[doc(hidden)]
1745pub mod spi;
1746
1747// Re-export macros so they are visible from the `led_strip` module path.
1748pub use crate::{
1749    __led_strip_dispatch_default_engine, __led_strip_dispatch_engine,
1750    __led_strip_dispatch_rmt_engine, __led_strip_first_or_default, __led_strip_impl,
1751    __led_strip_inner, __led_strip_parse_options, __led2d_strip_methods, __led2d_strip_trait_impl,
1752};