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