Skip to main content

device_envoy/
led2d.rs

1#![cfg_attr(
2    feature = "doc-images",
3    doc = ::embed_doc_image::embed_image!("led2d1", "docs/assets/led2d1.png"),
4    doc = ::embed_doc_image::embed_image!("led2d2", "docs/assets/led2d2.png")
5)]
6//! A device abstraction for rectangular NeoPixel-style (WS2812) LED panel displays.
7//! For 1-dimensional LED strips, see the [`led_strip`](mod@crate::led_strip) module.
8//!
9//! This page provides the primary documentation and examples for programming LED panels.
10//! The device abstraction supports text, graphics, and animation.
11//!
12//! **After reading the examples below, see also:**
13//!
14//! - [`led2d!`](macro@crate::led2d) — Macro to generate an LED-panel struct type (includes syntax details). See [`Led2dGenerated`](`crate::led2d::led2d_generated::Led2dGenerated`) for a sample of a generated type.
15//! - [`Led2dGenerated`](`crate::led2d::led2d_generated::Led2dGenerated`) — Sample struct type generated by the [`led2d!`](macro@crate::led2d) macro, showing all methods and constants.
16//! - [`LedLayout`] — Compile-type description of panel geometry and wiring, including dimensions (with examples)
17//! - [`Frame2d`] — 2D pixel array used for general graphics (includes examples)
18//! - [`led_strips!`](crate::led_strips) — Alternative macro to share a PIO resource with other panels or LED strips (includes examples)
19//!
20//! # Example: Write Text
21//!
22//! In this example, we render text on a 12×4 panel. Here, the generated struct type is named `Led12x4`.
23//!
24//! ![LED panel preview][led2d1]
25//!
26//! ```rust,no_run
27//! # #![no_std]
28//! # #![no_main]
29//! # use panic_probe as _;
30//! # use core::convert::Infallible;
31//! # use core::future;
32//! # use core::result::Result::Ok;
33//! # use embassy_executor::Spawner;
34//! # use embassy_rp::init;
35//! use device_envoy::{Result, led2d, led2d::layout::LedLayout, led2d::Led2dFont, led_strip::colors};
36//!
37//! // Tells us how the LED strip is wired up in the panel
38//! // in this case, a common snake-like pattern.
39//! const LED_LAYOUT_12X4: LedLayout<48, 12, 4> = LedLayout::serpentine_column_major();
40//!
41//! // Generate a type named `Led12x4`.
42//! led2d! {
43//!     Led12x4 {
44//!         pin: PIN_3,                          // GPIO pin for LED data signal
45//!         led_layout: LED_LAYOUT_12X4,         // LED layout mapping (defines dimensions)
46//!         font: Led2dFont::Font3x4Trim,        // Font variant
47//!     }
48//! }
49//!
50//! # #[embassy_executor::main]
51//! # pub async fn main(spawner: Spawner) -> ! {
52//! #     let err = example(spawner).await.unwrap_err();
53//! #     core::panic!("{err}");
54//! # }
55//! async fn example(spawner: Spawner) -> Result<Infallible> {
56//!     let p = init(Default::default());
57//!
58//!     // Create a device abstraction for the LED panel.
59//!     // Behind the scenes, this creates a channel & background task to manage the display.
60//!     let led12x4 = Led12x4::new(p.PIN_3, p.PIO0, p.DMA_CH0, spawner)?;
61//!
62//!     // Write text to the display with per-character colors.
63//!     let colors = [colors::CYAN, colors::RED, colors::YELLOW];
64//!     // Each character takes the next color; when we run out, we start over.
65//!     led12x4.write_text("Rust", &colors).await?;
66//!
67//!     future::pending().await // run forever
68//! }
69//! ```
70//!
71//! # Example: Animated Text on a Rotated Panel
72//!
73//! This example animates text on a rotated 12×8 panel built from two stacked 12×4 panels.
74//!
75//! ![LED panel preview][led2d2]
76//!
77//! ```rust,no_run
78//! # #![no_std]
79//! # #![no_main]
80//! # use panic_probe as _;
81//! # use core::convert::Infallible;
82//! # use core::future;
83//! # use embassy_executor::Spawner;
84//! # use embassy_rp::init;
85//! use device_envoy::{Result, led2d, led2d::layout::LedLayout, led2d::Frame2d, led2d::Led2dFont, led_strip::{Current, Gamma, colors}};
86//! use embassy_time::Duration;
87//!
88//! // Our panel is two 12x4 panels stacked vertically and then rotated clockwise.
89//! const LED_LAYOUT_12X4: LedLayout<48, 12, 4> = LedLayout::serpentine_column_major();
90//! const LED_LAYOUT_12X8: LedLayout<96, 12, 8> = LED_LAYOUT_12X4.combine_v(LED_LAYOUT_12X4);
91//! const LED_LAYOUT_12X8_ROTATED: LedLayout<96, 8, 12> = LED_LAYOUT_12X8.rotate_cw();
92//!
93//! // Generate a type named `Led12x8Animated`.
94//! led2d! {
95//!     pub(self) Led12x8Animated {               // Can provide a visibility modifier
96//!         pin: PIN_4,                           // GPIO pin for LED data signal
97//!         led_layout: LED_LAYOUT_12X8_ROTATED,  // Two 12×4 panels stacked and rotated
98//!         font: Led2dFont::Font4x6Trim,         // Use a 4x6 pixel font without the usual 1 pixel padding
99//!         pio: PIO1,                            // PIO resource, default is PIO0
100//!         dma: DMA_CH1,                         // DMA resource, default is DMA_CH0
101//!         max_current: Current::Milliamps(300), // Power budget, default is 250 mA.
102//!         gamma: Gamma::Linear,                 // Color correction curve, default is Gamma::Srgb
103//!         max_frames: 2,                        // maximum animation frames, default is 16
104//!     }
105//! }
106//!
107//! # #[embassy_executor::main]
108//! # pub async fn main(spawner: Spawner) -> ! {
109//! #     let err = example(spawner).await.unwrap_err();
110//! #     core::panic!("{err}");
111//! # }
112//! async fn example(spawner: Spawner) -> Result<Infallible> {
113//!     let p = init(Default::default());
114//!
115//!    // Create a device abstraction for the rotated LED panel.
116//!     let led_12x8_animated = Led12x8Animated::new(p.PIN_4, p.PIO1, p.DMA_CH1, spawner)?;
117//!
118//!     // Write "Go" into an in-memory frame buffer.
119//!     let mut frame_0 = Frame2d::new();
120//!     // Empty text colors array defaults to white.
121//!     led_12x8_animated.write_text_to_frame("Go", &[], &mut frame_0)?;
122//!
123//!     // Write "Go" into a second frame buffer with custom colors and on the 2nd line.
124//!     let mut frame_1 = Frame2d::new();
125//!     // "/n" starts a new line. Text does not wrap but rather clips.
126//!     led_12x8_animated.write_text_to_frame(
127//!         "\nGo",
128//!         &[colors::HOT_PINK, colors::LIME],
129//!         &mut frame_1,
130//!     )?;
131//!
132//!     // Animate between the two frames indefinitely.
133//!     let frame_duration = Duration::from_secs(1);
134//!     led_12x8_animated
135//!         .animate([(frame_0, frame_duration), (frame_1, frame_duration)])?;
136//!
137//!     future::pending().await // run forever
138//! }
139//! ```
140
141// Re-export for macro use
142#[doc(hidden)]
143pub use paste;
144
145/// Re-exported from the [`embedded-graphics`](https://docs.rs/embedded-graphics) crate.
146///
147/// # [`embedded-graphics::Size`](https://docs.rs/embedded-graphics/latest/embedded_graphics/geometry/struct.Point.html) Documentation:
148pub use embedded_graphics::geometry::Point;
149/// Re-exported from the [`embedded-graphics`](https://docs.rs/embedded-graphics) crate.
150///
151/// # [`embedded-graphics::Size`](https://docs.rs/embedded-graphics/latest/embedded_graphics/geometry/struct.Size.html) Documentation:
152pub use embedded_graphics::geometry::Size;
153
154pub mod layout;
155
156pub mod led2d_generated;
157
158pub use layout::LedLayout;
159
160use core::{
161    borrow::Borrow,
162    convert::Infallible,
163    ops::{Deref, DerefMut, Index, IndexMut},
164};
165use embassy_time::Duration;
166use embedded_graphics::pixelcolor::Rgb888;
167use embedded_graphics::{
168    draw_target::DrawTarget,
169    mono_font::{
170        DecorationDimensions, MonoFont,
171        ascii::{
172            FONT_4X6, FONT_5X7, FONT_5X8, FONT_6X9, FONT_6X10, FONT_6X12, FONT_6X13,
173            FONT_6X13_BOLD, FONT_6X13_ITALIC, FONT_7X13, FONT_7X13_BOLD, FONT_7X13_ITALIC,
174            FONT_7X14, FONT_7X14_BOLD, FONT_8X13, FONT_8X13_BOLD, FONT_8X13_ITALIC, FONT_9X15,
175            FONT_9X15_BOLD, FONT_9X18, FONT_9X18_BOLD, FONT_10X20,
176        },
177        mapping::StrGlyphMapping,
178    },
179    prelude::*,
180};
181use smart_leds::RGB8;
182
183#[cfg(not(feature = "host"))]
184use crate::led_strip::{Frame1d as StripFrame, LedStrip};
185#[cfg(feature = "host")]
186type StripFrame<const N: usize> = [RGB8; N];
187#[cfg(feature = "host")]
188/// Stub LED strip type for host testing.
189///
190/// This type provides no-op implementations for testing 2D LED panel code on host machines.
191/// See the [`led2d`](self) module documentation for usage.
192pub struct LedStrip<const N: usize, const MAX_FRAMES: usize>;
193#[cfg(feature = "host")]
194impl<const N: usize, const MAX_FRAMES: usize> LedStrip<N, MAX_FRAMES> {
195    fn write_frame(&self, _frame: StripFrame<N>) -> Result<()> {
196        Ok(())
197    }
198
199    fn animate(&self, _frames: impl IntoIterator<Item = (StripFrame<N>, Duration)>) -> Result<()> {
200        Ok(())
201    }
202}
203use crate::Result;
204use crate::led_strip::ToRgb888;
205
206// Packed bitmap for the internal 3x4 font (ASCII 0x20-0x7E).
207const BIT_MATRIX3X4_FONT_DATA: [u8; 144] = [
208    0x0a, 0xd5, 0x10, 0x4a, 0xa0, 0x01, 0x0a, 0xfe, 0x68, 0x85, 0x70, 0x02, 0x08, 0x74, 0x90, 0x86,
209    0xa5, 0xc4, 0x08, 0x5e, 0x68, 0x48, 0x08, 0x10, 0xeb, 0x7b, 0xe7, 0xfd, 0x22, 0x27, 0xb8, 0x9b,
210    0x39, 0xb4, 0x05, 0xd1, 0xa9, 0x3e, 0xea, 0x5d, 0x28, 0x0a, 0xff, 0xf3, 0xfc, 0xe4, 0x45, 0xd2,
211    0xff, 0x7d, 0xff, 0xbc, 0xd9, 0xff, 0xb7, 0xcb, 0xb4, 0xe8, 0xe9, 0xfd, 0xfe, 0xcb, 0x25, 0xaa,
212    0xd9, 0x7d, 0x97, 0x7d, 0xe7, 0xbf, 0xdf, 0x6f, 0xdf, 0x7f, 0x6d, 0xb7, 0xe0, 0xd0, 0xf7, 0xe5,
213    0x6d, 0x48, 0xc0, 0x68, 0xdf, 0x35, 0x6f, 0x49, 0x40, 0x40, 0x86, 0xf5, 0xd7, 0xab, 0xe0, 0xc7,
214    0x5f, 0x7d, 0xff, 0xbc, 0xd9, 0xff, 0x37, 0xcb, 0xb4, 0xe8, 0xe9, 0xfd, 0x1e, 0xcb, 0x25, 0xaa,
215    0xd9, 0x7d, 0x17, 0x7d, 0xe7, 0xbf, 0xdf, 0x6f, 0xdf, 0x7f, 0x6d, 0xb7, 0xb1, 0x80, 0xf7, 0xe5,
216    0x6d, 0x48, 0xa0, 0xa8, 0xdf, 0x35, 0x6f, 0x49, 0x20, 0x90, 0x86, 0xf5, 0xd7, 0xab, 0xb1, 0x80,
217];
218const BIT_MATRIX3X4_IMAGE_WIDTH: u32 = 48;
219const BIT_MATRIX3X4_GLYPH_MAPPING: StrGlyphMapping<'static> = StrGlyphMapping::new("\0 \u{7e}", 0);
220
221#[doc(hidden)]
222/// Monospace 3x4 font matching `bit_matrix3x4`.
223#[must_use]
224pub fn bit_matrix3x4_font() -> MonoFont<'static> {
225    MonoFont {
226        image: embedded_graphics::image::ImageRaw::new(
227            &BIT_MATRIX3X4_FONT_DATA,
228            BIT_MATRIX3X4_IMAGE_WIDTH,
229        ),
230        glyph_mapping: &BIT_MATRIX3X4_GLYPH_MAPPING,
231        character_size: embedded_graphics::prelude::Size::new(3, 4),
232        character_spacing: 0,
233        baseline: 3,
234        underline: DecorationDimensions::new(3, 1),
235        strikethrough: DecorationDimensions::new(2, 1),
236    }
237}
238
239#[doc(hidden)]
240/// Render text into a frame using the provided font.
241pub fn render_text_to_frame<const W: usize, const H: usize>(
242    frame: &mut Frame2d<W, H>,
243    font: &embedded_graphics::mono_font::MonoFont<'static>,
244    text: &str,
245    colors: &[RGB8],
246    spacing_reduction: (i32, i32),
247) -> Result<()> {
248    let glyph_width = font.character_size.width as i32;
249    let glyph_height = font.character_size.height as i32;
250    let advance_x = glyph_width - spacing_reduction.0;
251    let advance_y = glyph_height - spacing_reduction.1;
252    let width_limit = W as i32;
253    let height_limit = H as i32;
254    if height_limit <= 0 || width_limit <= 0 {
255        return Ok(());
256    }
257    let baseline = font.baseline as i32;
258    let mut x = 0i32;
259    let mut y = baseline;
260    let mut color_index: usize = 0;
261
262    for ch in text.chars() {
263        if ch == '\n' {
264            x = 0;
265            y += advance_y;
266            if y - baseline >= height_limit {
267                break;
268            }
269            continue;
270        }
271
272        // Clip characters that exceed width limit (no wrapping until explicit \n)
273        if x + advance_x > width_limit {
274            continue;
275        }
276
277        let color = if colors.is_empty() {
278            smart_leds::colors::WHITE
279        } else {
280            colors[color_index % colors.len()]
281        };
282        color_index = color_index.wrapping_add(1);
283
284        let mut buf = [0u8; 4];
285        let slice = ch.encode_utf8(&mut buf);
286        let style = embedded_graphics::mono_font::MonoTextStyle::new(font, color.to_rgb888());
287        let position = embedded_graphics::prelude::Point::new(x, y);
288        embedded_graphics::Drawable::draw(
289            &embedded_graphics::text::Text::new(slice, position, style),
290            frame,
291        )
292        .expect("drawing into frame cannot fail");
293
294        x += advance_x;
295    }
296
297    Ok(())
298}
299
300/// Fonts available for use with [led2d module](mod@crate::led2d) panels.
301///
302/// Fonts with `Trim` suffix remove blank spacing to pack text more tightly on small displays.
303#[derive(Clone, Copy, Debug)]
304pub enum Led2dFont {
305    /// 3x4 monospace font, trimmed (compact layout).
306    Font3x4Trim,
307    /// 4x6 monospace font.
308    Font4x6,
309    /// 3x5 monospace font, trimmed (compact layout).
310    Font3x5Trim,
311    /// 5x7 monospace font.
312    Font5x7,
313    /// 4x6 monospace font, trimmed (compact layout).
314    Font4x6Trim,
315    /// 5x8 monospace font.
316    Font5x8,
317    /// 4x7 monospace font, trimmed (compact layout).
318    Font4x7Trim,
319    /// 6x9 monospace font.
320    Font6x9,
321    /// 5x8 monospace font, trimmed (compact layout).
322    Font5x8Trim,
323    /// 6x10 monospace font.
324    Font6x10,
325    /// 5x9 monospace font, trimmed (compact layout).
326    Font5x9Trim,
327    /// 6x12 monospace font.
328    Font6x12,
329    /// 5x11 monospace font, trimmed (compact layout).
330    Font5x11Trim,
331    /// 6x13 monospace font.
332    Font6x13,
333    /// 5x12 monospace font, trimmed (compact layout).
334    Font5x12Trim,
335    /// 6x13 bold monospace font.
336    Font6x13Bold,
337    /// 5x12 bold monospace font, trimmed (compact layout).
338    Font5x12TrimBold,
339    /// 6x13 italic monospace font.
340    Font6x13Italic,
341    /// 5x12 italic monospace font, trimmed (compact layout).
342    Font5x12TrimItalic,
343    /// 7x13 monospace font.
344    Font7x13,
345    /// 6x12 monospace font, trimmed (compact layout).
346    Font6x12Trim,
347    /// 7x13 bold monospace font.
348    Font7x13Bold,
349    /// 6x12 bold monospace font, trimmed (compact layout).
350    Font6x12TrimBold,
351    /// 7x13 italic monospace font.
352    Font7x13Italic,
353    /// 6x12 italic monospace font, trimmed (compact layout).
354    Font6x12TrimItalic,
355    /// 7x14 monospace font.
356    Font7x14,
357    /// 6x13 monospace font, trimmed (compact layout).
358    Font6x13Trim,
359    /// 7x14 bold monospace font.
360    Font7x14Bold,
361    /// 6x13 bold monospace font, trimmed (compact layout).
362    Font6x13TrimBold,
363    /// 8x13 monospace font.
364    Font8x13,
365    /// 7x12 monospace font, trimmed (compact layout).
366    Font7x12Trim,
367    /// 8x13 bold monospace font.
368    Font8x13Bold,
369    /// 7x12 bold monospace font, trimmed (compact layout).
370    Font7x12TrimBold,
371    /// 8x13 italic monospace font.
372    Font8x13Italic,
373    /// 7x12 italic monospace font, trimmed (compact layout).
374    Font7x12TrimItalic,
375    /// 9x15 monospace font.
376    Font9x15,
377    /// 8x14 monospace font, trimmed (compact layout).
378    Font8x14Trim,
379    /// 9x15 bold monospace font.
380    Font9x15Bold,
381    /// 8x14 bold monospace font, trimmed (compact layout).
382    Font8x14TrimBold,
383    /// 9x18 monospace font.
384    Font9x18,
385    /// 8x17 monospace font, trimmed (compact layout).
386    Font8x17Trim,
387    /// 9x18 bold monospace font.
388    Font9x18Bold,
389    /// 8x17 bold monospace font, trimmed (compact layout).
390    Font8x17TrimBold,
391    /// 10x20 monospace font.
392    Font10x20,
393    /// 9x19 monospace font, trimmed (compact layout).
394    Font9x19Trim,
395}
396
397impl Led2dFont {
398    /// Return the `MonoFont` for this variant.
399    #[must_use]
400    pub fn to_font(self) -> MonoFont<'static> {
401        match self {
402            Self::Font3x4Trim => bit_matrix3x4_font(),
403            Self::Font4x6 | Self::Font3x5Trim => FONT_4X6,
404            Self::Font5x7 | Self::Font4x6Trim => FONT_5X7,
405            Self::Font5x8 | Self::Font4x7Trim => FONT_5X8,
406            Self::Font6x9 | Self::Font5x8Trim => FONT_6X9,
407            Self::Font6x10 | Self::Font5x9Trim => FONT_6X10,
408            Self::Font6x12 | Self::Font5x11Trim => FONT_6X12,
409            Self::Font6x13 | Self::Font5x12Trim => FONT_6X13,
410            Self::Font6x13Bold | Self::Font5x12TrimBold => FONT_6X13_BOLD,
411            Self::Font6x13Italic | Self::Font5x12TrimItalic => FONT_6X13_ITALIC,
412            Self::Font7x13 | Self::Font6x12Trim => FONT_7X13,
413            Self::Font7x13Bold | Self::Font6x12TrimBold => FONT_7X13_BOLD,
414            Self::Font7x13Italic | Self::Font6x12TrimItalic => FONT_7X13_ITALIC,
415            Self::Font7x14 | Self::Font6x13Trim => FONT_7X14,
416            Self::Font7x14Bold | Self::Font6x13TrimBold => FONT_7X14_BOLD,
417            Self::Font8x13 | Self::Font7x12Trim => FONT_8X13,
418            Self::Font8x13Bold | Self::Font7x12TrimBold => FONT_8X13_BOLD,
419            Self::Font8x13Italic | Self::Font7x12TrimItalic => FONT_8X13_ITALIC,
420            Self::Font9x15 | Self::Font8x14Trim => FONT_9X15,
421            Self::Font9x15Bold | Self::Font8x14TrimBold => FONT_9X15_BOLD,
422            Self::Font9x18 | Self::Font8x17Trim => FONT_9X18,
423            Self::Font9x18Bold | Self::Font8x17TrimBold => FONT_9X18_BOLD,
424            Self::Font10x20 | Self::Font9x19Trim => FONT_10X20,
425        }
426    }
427
428    /// Return spacing reduction for trimmed variants (width, height).
429    #[must_use]
430    pub const fn spacing_reduction(self) -> (i32, i32) {
431        match self {
432            Self::Font3x4Trim
433            | Self::Font4x6
434            | Self::Font5x7
435            | Self::Font5x8
436            | Self::Font6x9
437            | Self::Font6x10
438            | Self::Font6x12
439            | Self::Font6x13
440            | Self::Font6x13Bold
441            | Self::Font6x13Italic
442            | Self::Font7x13
443            | Self::Font7x13Bold
444            | Self::Font7x13Italic
445            | Self::Font7x14
446            | Self::Font7x14Bold
447            | Self::Font8x13
448            | Self::Font8x13Bold
449            | Self::Font8x13Italic
450            | Self::Font9x15
451            | Self::Font9x15Bold
452            | Self::Font9x18
453            | Self::Font9x18Bold
454            | Self::Font10x20 => (0, 0),
455            Self::Font3x5Trim
456            | Self::Font4x6Trim
457            | Self::Font4x7Trim
458            | Self::Font5x8Trim
459            | Self::Font5x9Trim
460            | Self::Font5x11Trim
461            | Self::Font5x12Trim
462            | Self::Font5x12TrimBold
463            | Self::Font5x12TrimItalic
464            | Self::Font6x12Trim
465            | Self::Font6x12TrimBold
466            | Self::Font6x12TrimItalic
467            | Self::Font6x13Trim
468            | Self::Font6x13TrimBold
469            | Self::Font7x12Trim
470            | Self::Font7x12TrimBold
471            | Self::Font7x12TrimItalic
472            | Self::Font8x14Trim
473            | Self::Font8x14TrimBold
474            | Self::Font8x17Trim
475            | Self::Font8x17TrimBold
476            | Self::Font9x19Trim => (1, 1),
477        }
478    }
479}
480
481/// 2D pixel array used for general graphics on LED panels (includes examples).
482///
483/// This page provides the primary documentation for drawing onto LED panels.
484///
485/// **Read the examples below first.** After that, keep these details in mind:
486///
487/// - Use a frame to prepare an image before sending it to the panel.
488/// - Coordinates are `(x, y)` with `(0, 0)` at the top-left. The x-axis increases to the right,
489///   and the y-axis increases downward.
490/// - Set pixels using tuple indexing: `frame[(x, y)] = colors::RED;`.
491/// - For shapes, lines, and text rendering, use the [`embedded-graphics`](https://docs.rs/embedded-graphics) crate.
492/// - Frames are rendered by a panel type generated with [`led2d!`](macro@crate::led2d).
493///   See [`Led2dGenerated`](crate::led2d::led2d_generated::Led2dGenerated) for the full API of the generated panel type.
494/// - For animation, call [`animate`](crate::led2d::led2d_generated::Led2dGenerated::animate) with a sequence
495///   of `(`[`Frame2d`]`, `[`Duration`](https://docs.rs/embassy-time/latest/embassy_time/struct.Duration.html)`)`
496///   pairs. See the [led2d](mod@crate::led2d) module for an example.
497///
498/// ## Indexing and storage
499///
500/// `Frame2d` supports both:
501///
502/// - `(x, y)` tuple indexing: `frame[(x, y)]`
503/// - Row-major array indexing: `frame[y][x]`
504///
505/// Tuple indexing matches display coordinates. Array indexing matches the underlying storage.
506///
507/// ## Rendering pipeline (what happens when you display a frame)
508///
509/// `Frame2d` is only pixel storage. When you render a frame through a generated panel type,
510/// the device abstraction:
511///
512/// - Maps `(x, y)` pixels to the physical LED wiring order
513/// - Applies gamma correction
514/// - Scales brightness to respect the configured electrical current budget
515///
516/// These steps are implemented using two **compile-time–generated lookup tables**.
517/// Writing a frame performs only indexed memory reads and writes.
518///
519/// # Example: Draw pixels both directly and with [`embedded-graphics`](https://docs.rs/embedded-graphics):
520///
521/// ![LED panel preview][led2d-graphics]
522///
523/// ```rust,no_run
524/// # #![no_std]
525/// # #![no_main]
526/// # use panic_probe as _;
527/// use device_envoy::{led2d::Frame2d, led_strip::ToRgb888};
528/// use embedded_graphics::{
529///     prelude::*,
530///     primitives::{Circle, PrimitiveStyle, Rectangle},
531/// };
532/// use smart_leds::colors;
533/// # fn example() {
534///
535/// type Frame = Frame2d<12, 8>;
536///
537/// /// Calculate the top-left corner position to center a shape within a bounding box.
538/// const fn centered_top_left(width: usize, height: usize, size: usize) -> Point {
539///     assert!(size <= width);
540///     assert!(size <= height);
541///     Point::new(((width - size) / 2) as i32, ((height - size) / 2) as i32)
542/// }
543///
544/// // Create a frame to draw on. This is just an in-memory 2D pixel buffer.
545/// let mut frame = Frame::new();
546///
547/// // Use the embedded-graphics crate to draw a red rectangle border around the edge of the frame.
548/// // We use `to_rgb888()` to convert from smart-leds RGB8 to embedded-graphics Rgb888.
549/// Rectangle::new(Frame::TOP_LEFT, Frame::SIZE)
550///     .into_styled(PrimitiveStyle::with_stroke(colors::RED.to_rgb888(), 1))
551///     .draw(&mut frame)
552///     .expect("rectangle draw must succeed");
553///
554/// // Direct pixel access: set the upper-left LED pixel (x = 0, y = 0).
555/// // Frame2d stores LED colors directly, so we write an LED color here.
556/// frame[(0, 0)] = colors::CYAN;
557///
558/// // Use the embedded-graphics crate to draw a green circle centered in the frame.
559/// const DIAMETER: u32 = 6;
560/// const CIRCLE_TOP_LEFT: Point = centered_top_left(Frame::WIDTH, Frame::HEIGHT, DIAMETER as usize);
561/// Circle::new(CIRCLE_TOP_LEFT, DIAMETER)
562///     .into_styled(PrimitiveStyle::with_stroke(colors::LIME.to_rgb888(), 1))
563///     .draw(&mut frame)
564///     .expect("circle draw must succeed");
565/// # }
566/// ```
567#[cfg_attr(
568    feature = "doc-images",
569    doc = ::embed_doc_image::embed_image!("led2d-graphics", "docs/assets/led2d_graphics.png")
570)]
571#[derive(Clone, Copy, Debug)]
572pub struct Frame2d<const W: usize, const H: usize>(pub [[RGB8; W]; H]);
573
574impl<const W: usize, const H: usize> Frame2d<W, H> {
575    /// The width of the frame.
576    pub const WIDTH: usize = W;
577    /// The height of the frame.
578    pub const HEIGHT: usize = H;
579    /// Total pixels in this frame (width × height).
580    pub const LEN: usize = W * H;
581    /// Frame dimensions as a [`Size`].
582    ///
583    /// For [`embedded-graphics`](https://docs.rs/embedded-graphics) drawing operation.
584    pub const SIZE: Size = Size::new(W as u32, H as u32);
585    /// Top-left corner coordinate as a [`Point`].
586    ///
587    /// For [`embedded-graphics`](https://docs.rs/embedded-graphics) drawing operation.
588    pub const TOP_LEFT: Point = Point::new(0, 0);
589    /// Top-right corner coordinate as a [`Point`].
590    ///
591    /// For [`embedded-graphics`](https://docs.rs/embedded-graphics) drawing operation.
592    pub const TOP_RIGHT: Point = Point::new((W - 1) as i32, 0);
593    /// Bottom-left corner coordinate as a [`Point`].
594    ///
595    /// For [`embedded-graphics`](https://docs.rs/embedded-graphics) drawing operation.
596    pub const BOTTOM_LEFT: Point = Point::new(0, (H - 1) as i32);
597    /// Bottom-right corner coordinate as a [`Point`].
598    ///
599    /// For [`embedded-graphics`](https://docs.rs/embedded-graphics) drawing operation.
600    pub const BOTTOM_RIGHT: Point = Point::new((W - 1) as i32, (H - 1) as i32);
601
602    /// Create a new blank (all black) frame.
603    #[must_use]
604    pub const fn new() -> Self {
605        Self([[RGB8::new(0, 0, 0); W]; H])
606    }
607
608    /// Create a frame filled with a single color.
609    #[must_use]
610    pub const fn filled(color: RGB8) -> Self {
611        Self([[color; W]; H])
612    }
613}
614
615impl<const W: usize, const H: usize> Deref for Frame2d<W, H> {
616    type Target = [[RGB8; W]; H];
617
618    fn deref(&self) -> &Self::Target {
619        &self.0
620    }
621}
622
623impl<const W: usize, const H: usize> DerefMut for Frame2d<W, H> {
624    fn deref_mut(&mut self) -> &mut Self::Target {
625        &mut self.0
626    }
627}
628
629impl<const W: usize, const H: usize> Index<(usize, usize)> for Frame2d<W, H> {
630    type Output = RGB8;
631
632    fn index(&self, (x_index, y_index): (usize, usize)) -> &Self::Output {
633        assert!(x_index < W, "x_index must be within width");
634        assert!(y_index < H, "y_index must be within height");
635        &self.0[y_index][x_index]
636    }
637}
638
639impl<const W: usize, const H: usize> IndexMut<(usize, usize)> for Frame2d<W, H> {
640    fn index_mut(&mut self, (x_index, y_index): (usize, usize)) -> &mut Self::Output {
641        assert!(x_index < W, "x_index must be within width");
642        assert!(y_index < H, "y_index must be within height");
643        &mut self.0[y_index][x_index]
644    }
645}
646
647impl<const W: usize, const H: usize> From<[[RGB8; W]; H]> for Frame2d<W, H> {
648    fn from(array: [[RGB8; W]; H]) -> Self {
649        Self(array)
650    }
651}
652
653impl<const W: usize, const H: usize> From<Frame2d<W, H>> for [[RGB8; W]; H] {
654    fn from(frame: Frame2d<W, H>) -> Self {
655        frame.0
656    }
657}
658
659impl<const W: usize, const H: usize> Default for Frame2d<W, H> {
660    fn default() -> Self {
661        Self::new()
662    }
663}
664
665impl<const W: usize, const H: usize> OriginDimensions for Frame2d<W, H> {
666    fn size(&self) -> Size {
667        Size::new(W as u32, H as u32)
668    }
669}
670
671impl<const W: usize, const H: usize> DrawTarget for Frame2d<W, H> {
672    type Color = Rgb888;
673    type Error = Infallible;
674
675    fn draw_iter<I>(&mut self, pixels: I) -> core::result::Result<(), Self::Error>
676    where
677        I: IntoIterator<Item = Pixel<Self::Color>>,
678    {
679        for Pixel(coord, color) in pixels {
680            let x_index = coord.x;
681            let y_index = coord.y;
682            if x_index >= 0 && x_index < W as i32 && y_index >= 0 && y_index < H as i32 {
683                self.0[y_index as usize][x_index as usize] =
684                    RGB8::new(color.r(), color.g(), color.b());
685            }
686        }
687        Ok(())
688    }
689}
690
691// Must be `pub` (not `pub(crate)`) because called by macro-generated code that expands at the call site in downstream crates.
692// This is an implementation detail, not part of the user-facing API.
693#[doc(hidden)]
694/// A device abstraction for rectangular NeoPixel-style (WS2812) LED matrix displays.
695///
696/// Supports any size display with arbitrary LED-index-to-coordinate mapping. The provided mapping
697/// is reversed during initialization into an internal (row, col) → LED index lookup so frame
698/// conversion stays fast.
699///
700/// Rows and columns are metadata used only for indexing - the core type is generic only over
701/// N (total LEDs) and MAX_FRAMES (animation capacity).
702///
703/// Most users should use the `led2d!` or `led2d_from_strip!` macros which generate
704/// a higher-level wrapper. See the [led2d](mod@crate::led2d) module docs for examples.
705pub struct Led2d<const N: usize, const MAX_FRAMES: usize> {
706    led_strip: &'static LedStrip<N, MAX_FRAMES>,
707    mapping_by_xy: [u16; N],
708    width: usize,
709}
710
711impl<const N: usize, const MAX_FRAMES: usize> Led2d<N, MAX_FRAMES> {
712    /// Create Led2d device handle.
713    ///
714    /// The `led_layout` defines how LED indices map to `(column, row)` coordinates. Entry `i`
715    /// provides the `(col, row)` destination for LED `i`. The layout is inverted via
716    /// [`LedLayout::xy_to_index`] so (row, col) queries are O(1) when converting frames.
717    ///
718    /// See the [Led2d struct example](Self) for usage.
719    #[must_use]
720    pub fn new<const W: usize, const H: usize>(
721        led_strip: &'static LedStrip<N, MAX_FRAMES>,
722        led_layout: &LedLayout<N, W, H>,
723    ) -> Self {
724        assert_eq!(
725            W.checked_mul(H).expect("width * height must fit in usize"),
726            N,
727            "width * height must equal N (total LEDs for led_layout reversal)"
728        );
729        Self {
730            led_strip,
731            mapping_by_xy: led_layout.xy_to_index(),
732            width: W,
733        }
734    }
735
736    /// Convert (column, row) coordinates to LED strip index using the stored LED layout.
737    #[must_use]
738    fn xy_to_index(&self, x_index: usize, y_index: usize) -> usize {
739        self.mapping_by_xy[y_index * self.width + x_index] as usize
740    }
741
742    /// Convert 2D frame to 1D array using the LED layout.
743    fn convert_frame<const W: usize, const H: usize>(
744        &self,
745        frame_2d: Frame2d<W, H>,
746    ) -> StripFrame<N> {
747        let mut frame_1d = [RGB8::new(0, 0, 0); N];
748        for y_index in 0..H {
749            for x_index in 0..W {
750                let led_index = self.xy_to_index(x_index, y_index);
751                frame_1d[led_index] = frame_2d[(x_index, y_index)];
752            }
753        }
754        StripFrame::from(frame_1d)
755    }
756
757    /// Render a fully defined frame to the panel.
758    ///
759    /// Frame2d is a 2D array in row-major order where `frame[(col, row)]` is the pixel at (col, row).
760    pub fn write_frame<const W: usize, const H: usize>(&self, frame: Frame2d<W, H>) -> Result<()> {
761        let strip_frame = self.convert_frame(frame);
762        self.led_strip.write_frame(strip_frame)
763    }
764
765    /// Loop through a sequence of animation frames until interrupted by another command.
766    ///
767    /// Each frame is a tuple of `(Frame2d, Duration)`. Accepts arrays, `Vec`s, or any
768    /// iterator that produces `(Frame2d, Duration)` tuples. For best efficiency with large
769    /// frame sequences, pass an iterator to avoid intermediate allocations.
770    ///
771    /// Returns immediately; the animation runs in the background until interrupted
772    /// by a new `animate` call or `write_frame`.
773    pub fn animate<const W: usize, const H: usize, I>(&self, frames: I) -> Result<()>
774    where
775        I: IntoIterator,
776        I::Item: Borrow<(Frame2d<W, H>, Duration)>,
777    {
778        self.led_strip.animate(frames.into_iter().map(|frame| {
779            let (frame, duration) = *frame.borrow();
780            (self.convert_frame(frame), duration)
781        }))
782    }
783}
784
785/// Macro to generate an LED-panel struct type (includes syntax details). See [`Led2dGenerated`](`crate::led2d::led2d_generated::Led2dGenerated`) for a sample of a generated type.
786///
787/// **See the [led2d module](mod@crate::led2d) for usage examples.**
788///
789/// **Syntax:**
790///
791/// ```text
792/// led2d! {
793///     [<visibility>] <Name> {
794///         pin: <pin_ident>,
795///         led_layout: <LedLayout_expr>,
796///         font: <Led2dFont_expr>,
797///         pio: <pio_ident>,               // optional
798///         dma: <dma_ident>,               // optional
799///         max_current: <Current_expr>,    // optional
800///         gamma: <Gamma_expr>,            // optional
801///         max_frames: <usize_expr>,       // optional
802///     }
803/// }
804/// ```
805///
806/// # Fields
807///
808/// **Required fields:**
809///
810/// - `pin` — GPIO pin for LED data
811/// - `led_layout` — LED strip physical layout (see [`LedLayout`]); this defines the panel size
812/// - `font` — Built-in font variant (see [`Led2dFont`]), e.g. `Led2dFont::Font4x6Trim`.
813///   Bring `Led2dFont` into scope or use a full path like `device_envoy::led2d::Led2dFont::Font4x6Trim`.
814///
815/// The `led_layout` value must be a const so its dimensions can be derived at compile time.
816///
817/// **Optional fields:**
818///
819/// - `pio` — PIO resource to use (default: `PIO0`)
820/// - `dma` — DMA channel (default: `DMA_CH0`)
821/// - `max_current` — Electrical current budget (default: 250 mA)
822/// - `gamma` — Color curve (default: `Gamma::Srgb`)
823/// - `max_frames` — Maximum number of animation frames for the generated strip (default: 16 frames)
824///
825/// `max_frames = 0` disables animation and allocates no frame storage; `write_frame()` is still supported.
826///
827#[doc = include_str!("docs/current_limiting_and_gamma.md")]
828///
829/// # Related Macros
830///
831/// - [`led_strips!`](crate::led_strips) — Alternative macro to share a PIO resource with other panels or LED strips (includes examples)
832/// - [`led_strip!`](mod@crate::led_strip) — For 1-dimensional LED strips
833#[macro_export]
834#[cfg(not(feature = "host"))]
835#[doc(hidden)]
836macro_rules! led2d {
837    ($($tt:tt)*) => { $crate::__led2d_impl! { $($tt)* } };
838}
839
840/// Implementation macro. Not part of the public API; use [`led2d!`] instead.
841#[doc(hidden)] // Required pub for macro expansion in downstream crates
842#[macro_export]
843#[cfg(not(feature = "host"))]
844macro_rules! __led2d_impl {
845    // Legacy entry point - comma syntax (temporary for backward compatibility)
846    (
847        $name:ident,
848        $($fields:tt)*
849    ) => {
850        $crate::__led2d_impl! { pub $name, $($fields)* }
851    };
852
853    // Legacy entry point - comma syntax with visibility (temporary for backward compatibility)
854    (
855        $vis:vis $name:ident,
856        $($fields:tt)*
857    ) => {
858        $crate::__led2d_impl! {
859            @__fill_defaults
860            vis: $vis,
861            name: $name,
862            pio: PIO0,
863            pin: _UNSET_,
864            dma: DMA_CH0,
865            led_layout: _UNSET_,
866            max_current: _UNSET_,
867            gamma: $crate::led_strip::GAMMA_DEFAULT,
868            max_frames: $crate::led_strip::MAX_FRAMES_DEFAULT,
869            font: _UNSET_,
870            fields: [ $($fields)* ]
871        }
872    };
873
874    // Entry point - name without visibility defaults to private
875    (
876        $name:ident {
877            $($fields:tt)*
878        }
879    ) => {
880        $crate::__led2d_impl! {
881            @__fill_defaults
882            vis: pub(self),
883            name: $name,
884            pio: PIO0,
885            pin: _UNSET_,
886            dma: DMA_CH0,
887            led_layout: _UNSET_,
888            max_current: _UNSET_,
889            gamma: $crate::led_strip::GAMMA_DEFAULT,
890            max_frames: $crate::led_strip::MAX_FRAMES_DEFAULT,
891            font: _UNSET_,
892            fields: [ $($fields)* ]
893        }
894    };
895
896    // Entry point - name with explicit visibility
897    (
898        $vis:vis $name:ident {
899            $($fields:tt)*
900        }
901    ) => {
902        $crate::__led2d_impl! {
903            @__fill_defaults
904            vis: $vis,
905            name: $name,
906            pio: PIO0,
907            pin: _UNSET_,
908            dma: DMA_CH0,
909            led_layout: _UNSET_,
910            max_current: _UNSET_,
911            gamma: $crate::led_strip::GAMMA_DEFAULT,
912            max_frames: $crate::led_strip::MAX_FRAMES_DEFAULT,
913            font: _UNSET_,
914            fields: [ $($fields)* ]
915        }
916    };
917
918    // Fill defaults: pio
919    (@__fill_defaults
920        vis: $vis:vis,
921        name: $name:ident,
922        pio: $pio:ident,
923        pin: $pin:tt,
924        dma: $dma:ident,
925        led_layout: $led_layout:tt,
926        max_current: $max_current:tt,
927        gamma: $gamma:expr,
928        max_frames: $max_frames:expr,
929        font: $font_variant:tt,
930        fields: [ pio: $new_pio:ident $(, $($rest:tt)* )? ]
931    ) => {
932        $crate::__led2d_impl! {
933            @__fill_defaults
934            vis: $vis,
935            name: $name,
936            pio: $new_pio,
937            pin: $pin,
938            dma: $dma,
939            led_layout: $led_layout,
940            max_current: $max_current,
941            gamma: $gamma,
942            max_frames: $max_frames,
943            font: $font_variant,
944            fields: [ $($($rest)*)? ]
945        }
946    };
947
948    // Fill defaults: pin
949    (@__fill_defaults
950        vis: $vis:vis,
951        name: $name:ident,
952        pio: $pio:ident,
953        pin: $pin:tt,
954        dma: $dma:ident,
955        led_layout: $led_layout:tt,
956        max_current: $max_current:tt,
957        gamma: $gamma:expr,
958        max_frames: $max_frames:expr,
959        font: $font_variant:tt,
960        fields: [ pin: $new_pin:ident $(, $($rest:tt)* )? ]
961    ) => {
962        $crate::__led2d_impl! {
963            @__fill_defaults
964            vis: $vis,
965            name: $name,
966            pio: $pio,
967            pin: $new_pin,
968            dma: $dma,
969            led_layout: $led_layout,
970            max_current: $max_current,
971            gamma: $gamma,
972            max_frames: $max_frames,
973            font: $font_variant,
974            fields: [ $($($rest)*)? ]
975        }
976    };
977
978    // Fill defaults: dma
979    (@__fill_defaults
980        vis: $vis:vis,
981        name: $name:ident,
982        pio: $pio:ident,
983        pin: $pin:tt,
984        dma: $dma:ident,
985        led_layout: $led_layout:tt,
986        max_current: $max_current:tt,
987        gamma: $gamma:expr,
988        max_frames: $max_frames:expr,
989        font: $font_variant:tt,
990        fields: [ dma: $new_dma:ident $(, $($rest:tt)* )? ]
991    ) => {
992        $crate::__led2d_impl! {
993            @__fill_defaults
994            vis: $vis,
995            name: $name,
996            pio: $pio,
997            pin: $pin,
998            dma: $new_dma,
999            led_layout: $led_layout,
1000            max_current: $max_current,
1001            gamma: $gamma,
1002            max_frames: $max_frames,
1003            font: $font_variant,
1004            fields: [ $($($rest)*)? ]
1005        }
1006    };
1007
1008    // Fill defaults: led_layout
1009    (@__fill_defaults
1010        vis: $vis:vis,
1011        name: $name:ident,
1012        pio: $pio:ident,
1013        pin: $pin:tt,
1014        dma: $dma:ident,
1015        led_layout: $led_layout:tt,
1016        max_current: $max_current:tt,
1017        gamma: $gamma:expr,
1018        max_frames: $max_frames:expr,
1019        font: $font_variant:tt,
1020        fields: [ led_layout: $new_led_layout:tt $(, $($rest:tt)* )? ]
1021    ) => {
1022        $crate::__led2d_impl! {
1023            @__fill_defaults
1024            vis: $vis,
1025            name: $name,
1026            pio: $pio,
1027            pin: $pin,
1028            dma: $dma,
1029            led_layout: $new_led_layout,
1030            max_current: $max_current,
1031            gamma: $gamma,
1032            max_frames: $max_frames,
1033            font: $font_variant,
1034            fields: [ $($($rest)*)? ]
1035        }
1036    };
1037
1038    // Fill defaults: max_current
1039    (@__fill_defaults
1040        vis: $vis:vis,
1041        name: $name:ident,
1042        pio: $pio:ident,
1043        pin: $pin:tt,
1044        dma: $dma:ident,
1045        led_layout: $led_layout:tt,
1046        max_current: $max_current:tt,
1047        gamma: $gamma:expr,
1048        max_frames: $max_frames:expr,
1049        font: $font_variant:tt,
1050        fields: [ max_current: $new_max_current:expr $(, $($rest:tt)* )? ]
1051    ) => {
1052        $crate::__led2d_impl! {
1053            @__fill_defaults
1054            vis: $vis,
1055            name: $name,
1056            pio: $pio,
1057            pin: $pin,
1058            dma: $dma,
1059            led_layout: $led_layout,
1060            max_current: $new_max_current,
1061            gamma: $gamma,
1062            max_frames: $max_frames,
1063            font: $font_variant,
1064            fields: [ $($($rest)*)? ]
1065        }
1066    };
1067
1068    // Fill defaults: gamma
1069    (@__fill_defaults
1070        vis: $vis:vis,
1071        name: $name:ident,
1072        pio: $pio:ident,
1073        pin: $pin:tt,
1074        dma: $dma:ident,
1075        led_layout: $led_layout:tt,
1076        max_current: $max_current:tt,
1077        gamma: $gamma:expr,
1078        max_frames: $max_frames:expr,
1079        font: $font_variant:tt,
1080        fields: [ gamma: $new_gamma:expr $(, $($rest:tt)* )? ]
1081    ) => {
1082        $crate::__led2d_impl! {
1083            @__fill_defaults
1084            vis: $vis,
1085            name: $name,
1086            pio: $pio,
1087            pin: $pin,
1088            dma: $dma,
1089            led_layout: $led_layout,
1090            max_current: $max_current,
1091            gamma: $new_gamma,
1092            max_frames: $max_frames,
1093            font: $font_variant,
1094            fields: [ $($($rest)*)? ]
1095        }
1096    };
1097
1098    // Fill defaults: max_frames
1099    (@__fill_defaults
1100        vis: $vis:vis,
1101        name: $name:ident,
1102        pio: $pio:ident,
1103        pin: $pin:tt,
1104        dma: $dma:ident,
1105        led_layout: $led_layout:tt,
1106        max_current: $max_current:tt,
1107        gamma: $gamma:expr,
1108        max_frames: $max_frames:expr,
1109        font: $font_variant:tt,
1110        fields: [ max_frames: $new_max_frames:expr $(, $($rest:tt)* )? ]
1111    ) => {
1112        $crate::__led2d_impl! {
1113            @__fill_defaults
1114            vis: $vis,
1115            name: $name,
1116            pio: $pio,
1117            pin: $pin,
1118            dma: $dma,
1119            led_layout: $led_layout,
1120            max_current: $max_current,
1121            gamma: $gamma,
1122            max_frames: $new_max_frames,
1123            font: $font_variant,
1124            fields: [ $($($rest)*)? ]
1125        }
1126    };
1127
1128    // Fill defaults: font
1129    (@__fill_defaults
1130        vis: $vis:vis,
1131        name: $name:ident,
1132        pio: $pio:ident,
1133        pin: $pin:tt,
1134        dma: $dma:ident,
1135        led_layout: $led_layout:tt,
1136        max_current: $max_current:tt,
1137        gamma: $gamma:expr,
1138        max_frames: $max_frames:expr,
1139        font: $font_variant:tt,
1140        fields: [ font: $new_font_variant:expr $(, $($rest:tt)* )? ]
1141    ) => {
1142        $crate::__led2d_impl! {
1143            @__fill_defaults
1144            vis: $vis,
1145            name: $name,
1146            pio: $pio,
1147            pin: $pin,
1148            dma: $dma,
1149            led_layout: $led_layout,
1150            max_current: $max_current,
1151            gamma: $gamma,
1152            max_frames: $max_frames,
1153            font: $new_font_variant,
1154            fields: [ $($($rest)*)? ]
1155        }
1156    };
1157
1158    // Fill default max_current if still unset.
1159    (@__fill_defaults
1160        vis: $vis:vis,
1161        name: $name:ident,
1162        pio: $pio:ident,
1163        pin: $pin:tt,
1164        dma: $dma:ident,
1165        led_layout: $led_layout:tt,
1166        max_current: _UNSET_,
1167        gamma: $gamma:expr,
1168        max_frames: $max_frames:expr,
1169        font: $font_variant:tt,
1170        fields: [ ]
1171    ) => {
1172        $crate::__led2d_impl! {
1173            @__fill_defaults
1174            vis: $vis,
1175            name: $name,
1176            pio: $pio,
1177            pin: $pin,
1178            dma: $dma,
1179            led_layout: $led_layout,
1180            max_current: $crate::led_strip::MAX_CURRENT_DEFAULT,
1181            gamma: $gamma,
1182            max_frames: $max_frames,
1183            font: $font_variant,
1184            fields: [ ]
1185        }
1186    };
1187
1188    // Terminal: pass through once all fields consumed.
1189    (@__fill_defaults
1190        vis: $vis:vis,
1191        name: $name:ident,
1192        pio: $pio:ident,
1193        pin: $pin:tt,
1194        dma: $dma:ident,
1195        led_layout: $led_layout:tt,
1196        max_current: $max_current:expr,
1197        gamma: $gamma:expr,
1198        max_frames: $max_frames:expr,
1199        font: $font_variant:expr,
1200        fields: [ ]
1201    ) => {
1202        $crate::__led2d_impl! {
1203            @__expand
1204            vis: $vis,
1205            name: $name,
1206            pio: $pio,
1207            pin: $pin,
1208            dma: $dma,
1209            led_layout: $led_layout,
1210            max_current: $max_current,
1211            gamma: $gamma,
1212            max_frames: $max_frames,
1213            font: $font_variant
1214        }
1215    };
1216
1217    // Expand: custom led_layout variant (LedLayout expression).
1218    (@__expand
1219        vis: $vis:vis,
1220        name: $name:ident,
1221        pio: $pio:ident,
1222        pin: $pin:ident,
1223        dma: $dma:ident,
1224        led_layout: $led_layout:expr,
1225        max_current: $max_current:expr,
1226        gamma: $gamma:expr,
1227        max_frames: $max_frames:expr,
1228        font: $font_variant:expr
1229    ) => {
1230        $crate::led2d::paste::paste! {
1231            const [<$name:upper _LAYOUT>]: $crate::led2d::LedLayout<
1232                { $led_layout.len() },
1233                { $led_layout.width() },
1234                { $led_layout.height() }
1235            > = $led_layout;
1236
1237            // Generate the LED strip infrastructure with a CamelCase strip type
1238            $crate::__led_strips_impl! {
1239                @__with_frame_alias
1240                frame_alias: __SKIP_FRAME_ALIAS__,
1241                pio: $pio,
1242                vis: $vis,
1243                [<$name Strips>] {
1244                    [<$name LedStrip>]: {
1245                        dma: $dma,
1246                        pin: $pin,
1247                        len: { [<$name:upper _LAYOUT>].len() },
1248                        max_current: $max_current,
1249                        gamma: $gamma,
1250                        max_frames: $max_frames,
1251                    }
1252                }
1253            }
1254
1255            // Generate the Led2d device from the strip with custom mapping
1256            const [<$name:upper _MAX_FRAMES>]: usize = [<$name LedStrip>]::MAX_FRAMES;
1257
1258            // Compile-time assertion that strip length matches led_layout length
1259            const _: () = assert!([<$name:upper _LAYOUT>].index_to_xy().len() == [<$name LedStrip>]::LEN);
1260
1261            $crate::led2d::led2d_from_strip! {
1262                @__from_layout_const
1263                $vis $name,
1264                strip_type: [<$name LedStrip>],
1265                led_layout_const: [<$name:upper _LAYOUT>],
1266                font: $font_variant,
1267                max_frames_const: [<$name:upper _MAX_FRAMES>],
1268            }
1269
1270            // Add simplified constructor that handles PIO splitting and both statics
1271            #[allow(non_snake_case, dead_code)]
1272            impl [<$name>] {
1273                /// Create a new LED matrix display with automatic PIO setup.
1274                ///
1275                /// This is a convenience constructor that handles PIO splitting and static
1276                /// resource management automatically. All initialization happens in a single call.
1277                ///
1278                /// # Parameters
1279                ///
1280                /// - `pin`: GPIO pin for LED data signal
1281                /// - `pio`: PIO peripheral
1282                /// - `dma`: DMA channel for LED data transfer
1283                /// - `spawner`: Task spawner for background operations
1284                #[allow(non_upper_case_globals)]
1285                $vis fn new(
1286                    pin: ::embassy_rp::Peri<'static, ::embassy_rp::peripherals::$pin>,
1287                    pio: ::embassy_rp::Peri<'static, ::embassy_rp::peripherals::$pio>,
1288                    dma: ::embassy_rp::Peri<'static, ::embassy_rp::peripherals::$dma>,
1289                    spawner: ::embassy_executor::Spawner,
1290                ) -> $crate::Result<Self> {
1291                    // Split PIO into state machines (uses SM0 automatically)
1292                    let (sm0, _sm1, _sm2, _sm3) = [<$pio:lower _split>](pio);
1293
1294                    // Create strip (uses interior static)
1295                    let led_strip = [<$name LedStrip>]::new(
1296                        sm0,
1297                        pin,
1298                        dma,
1299                        spawner
1300                    )?;
1301
1302                    // Create Led2d from strip (uses interior static)
1303                    [<$name>]::from_strip(led_strip)
1304                }
1305            }
1306        }
1307    };
1308}
1309
1310// Internal macro used by led_strips! led2d configuration.
1311#[doc(hidden)] // Public for macro expansion in downstream crates; not a user-facing API.
1312#[macro_export]
1313#[cfg(not(feature = "host"))]
1314macro_rules! led2d_from_strip {
1315    // Serpentine column-major led_layout variant (uses strip's MAX_FRAMES)
1316    (
1317        $vis:vis $name:ident,
1318        strip_type: $strip_type:ident,
1319        width: $width:expr,
1320        height: $height:expr,
1321        led_layout: serpentine_column_major,
1322        font: $font_variant:expr $(,)?
1323    ) => {
1324        $crate::led2d::paste::paste! {
1325            const [<$name:upper _LED_LAYOUT>]: $crate::led2d::LedLayout<{ $width * $height }, { $width }, { $height }> =
1326                $crate::led2d::LedLayout::<{ $width * $height }, { $width }, { $height }>::serpentine_column_major();
1327            const [<$name:upper _MAX_FRAMES>]: usize = $strip_type::MAX_FRAMES;
1328
1329            // Compile-time assertion that strip length matches led_layout length
1330            const _: () = assert!([<$name:upper _LED_LAYOUT>].index_to_xy().len() == $strip_type::LEN);
1331
1332            $crate::led2d::led2d_from_strip!(
1333                @common $vis, $name, $strip_type, [<$name:upper _LED_LAYOUT>],
1334                $font_variant,
1335                [<$name:upper _MAX_FRAMES>]
1336            );
1337        }
1338    };
1339    // Custom led_layout variant (uses strip's MAX_FRAMES)
1340    (
1341        $vis:vis $name:ident,
1342        strip_type: $strip_type:ident,
1343        width: $width:expr,
1344        height: $height:expr,
1345        led_layout: $led_layout:expr,
1346        font: $font_variant:expr $(,)?
1347    ) => {
1348        $crate::led2d::paste::paste! {
1349            const [<$name:upper _LED_LAYOUT>]: $crate::led2d::LedLayout<{ $width * $height }, { $width }, { $height }> = $led_layout;
1350            const [<$name:upper _MAX_FRAMES>]: usize = $strip_type::MAX_FRAMES;
1351
1352            // Compile-time assertion that strip length matches led_layout length
1353            const _: () = assert!([<$name:upper _LED_LAYOUT>].index_to_xy().len() == $strip_type::LEN);
1354
1355            $crate::led2d::led2d_from_strip!(
1356                @common $vis, $name, $strip_type, [<$name:upper _LED_LAYOUT>],
1357                $font_variant,
1358                [<$name:upper _MAX_FRAMES>]
1359            );
1360        }
1361    };
1362    // Internal: use existing led_layout const (avoids redundant constants)
1363    (
1364        @__from_layout_const
1365        $vis:vis $name:ident,
1366        strip_type: $strip_type:ident,
1367        led_layout_const: $led_layout_const:ident,
1368        font: $font_variant:expr,
1369        max_frames_const: $max_frames_const:ident $(,)?
1370    ) => {
1371        $crate::led2d::led2d_from_strip!(
1372            @common $vis, $name, $strip_type, $led_layout_const,
1373            $font_variant,
1374            $max_frames_const
1375        );
1376    };
1377    // Common implementation (shared by both variants)
1378    (
1379        @common $vis:vis,
1380        $name:ident,
1381        $strip_type:ident,
1382        $led_layout_const:ident,
1383        $font_variant:expr,
1384        $max_frames_const:ident
1385    ) => {
1386        $crate::led2d::paste::paste! {
1387            /// LED matrix device handle generated by [`led2d_from_strip!`](crate::led2d::led2d_from_strip).
1388            $vis struct [<$name>] {
1389                led2d: $crate::led2d::Led2d<{ $led_layout_const.len() }, $max_frames_const>,
1390                font: embedded_graphics::mono_font::MonoFont<'static>,
1391                font_variant: $crate::led2d::Led2dFont,
1392            }
1393
1394            #[allow(non_snake_case, dead_code)]
1395            impl [<$name>] {
1396                /// Number of columns in the panel.
1397                pub const WIDTH: usize = $led_layout_const.width();
1398                /// Number of rows in the panel.
1399                pub const HEIGHT: usize = $led_layout_const.height();
1400                /// Total number of LEDs (WIDTH * HEIGHT).
1401                pub const N: usize = $led_layout_const.len();
1402                /// Frame dimensions as a [`Size`] for embedded-graphics.
1403                pub const SIZE: $crate::led2d::Size = $crate::led2d::Frame2d::<{ $led_layout_const.width() }, { $led_layout_const.height() }>::SIZE;
1404                /// Top-left corner coordinate for embedded-graphics drawing.
1405                pub const TOP_LEFT: $crate::led2d::Point = $crate::led2d::Frame2d::<{ $led_layout_const.width() }, { $led_layout_const.height() }>::TOP_LEFT;
1406                /// Top-right corner coordinate for embedded-graphics drawing.
1407                pub const TOP_RIGHT: $crate::led2d::Point = $crate::led2d::Frame2d::<{ $led_layout_const.width() }, { $led_layout_const.height() }>::TOP_RIGHT;
1408                /// Bottom-left corner coordinate for embedded-graphics drawing.
1409                pub const BOTTOM_LEFT: $crate::led2d::Point = $crate::led2d::Frame2d::<{ $led_layout_const.width() }, { $led_layout_const.height() }>::BOTTOM_LEFT;
1410                /// Bottom-right corner coordinate for embedded-graphics drawing.
1411                pub const BOTTOM_RIGHT: $crate::led2d::Point = $crate::led2d::Frame2d::<{ $led_layout_const.width() }, { $led_layout_const.height() }>::BOTTOM_RIGHT;
1412                /// Maximum number of animation frames supported for this device.
1413                pub const MAX_FRAMES: usize = $max_frames_const;
1414
1415                // Public so led2d_from_strip! expansions in downstream crates can call it.
1416                #[doc(hidden)]
1417                $vis fn from_strip(
1418                    led_strip: &'static $strip_type,
1419                ) -> $crate::Result<Self> {
1420                    let led2d = $crate::led2d::Led2d::new(
1421                        led_strip.as_ref(),
1422                        &$led_layout_const,
1423                    );
1424
1425                    defmt::info!("Led2d::new: device created successfully");
1426                    Ok(Self {
1427                        led2d,
1428                font: $font_variant.to_font(),
1429                font_variant: $font_variant,
1430            })
1431        }
1432
1433                /// Render a fully defined frame to the panel.
1434                $vis fn write_frame(
1435                    &self,
1436                    frame: $crate::led2d::Frame2d<{ $led_layout_const.width() }, { $led_layout_const.height() }>,
1437                ) -> $crate::Result<()> {
1438                    self.led2d.write_frame(frame)
1439                }
1440
1441                /// Loop through a sequence of animation frames. Pass arrays by value or Vecs/iters.
1442                $vis fn animate(
1443                    &self,
1444                    frames: impl IntoIterator<
1445                        Item = (
1446                            $crate::led2d::Frame2d<{ $led_layout_const.width() }, { $led_layout_const.height() }>,
1447                            ::embassy_time::Duration,
1448                        ),
1449                    >,
1450                ) -> $crate::Result<()> {
1451                    self.led2d.animate(frames)
1452                }
1453
1454                /// Render text into a frame using the configured font and spacing.
1455                pub fn write_text_to_frame(
1456                    &self,
1457                    text: &str,
1458                    colors: &[smart_leds::RGB8],
1459                    frame: &mut $crate::led2d::Frame2d<{ $led_layout_const.width() }, { $led_layout_const.height() }>,
1460                ) -> $crate::Result<()> {
1461                    $crate::led2d::render_text_to_frame(frame, &self.font, text, colors, self.font_variant.spacing_reduction())
1462                }
1463
1464                /// Render text and display it on the LED matrix.
1465                pub async fn write_text(&self, text: &str, colors: &[smart_leds::RGB8]) -> $crate::Result<()> {
1466                    let mut frame = $crate::led2d::Frame2d::<{ $led_layout_const.width() }, { $led_layout_const.height() }>::new();
1467                    self.write_text_to_frame(text, colors, &mut frame)?;
1468                    self.write_frame(frame)
1469                }
1470            }
1471        }
1472    };
1473}
1474
1475#[cfg(not(feature = "host"))]
1476#[doc(inline)]
1477pub use led2d;
1478#[cfg(not(feature = "host"))]
1479#[doc(hidden)] // Public for macro expansion in downstream crates; not a user-facing API.
1480pub use led2d_from_strip;