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;