rustyfarian_esp_hal_ws2812/lib.rs
1#![no_std]
2//! WS2812 (NeoPixel) LED driver using `esp-hal` RMT peripheral (bare-metal, `no_std`).
3//!
4//! This crate provides a bare-metal driver for WS2812/NeoPixel addressable LEDs
5//! using the `esp-hal` RMT peripheral.
6//! It is the `no_std` counterpart to `rustyfarian-esp-idf-ws2812`.
7//!
8//! Pure color utilities are available in the `bunting` crate for testing.
9//!
10//! # Buffer Sizing
11//!
12//! The driver uses a const-generic buffer `[PulseCode; N]` where `N = num_leds * 24 + 1`.
13//! Use [`buffer_size`] to compute `N` at compile time:
14//!
15//! ```ignore
16//! use rustyfarian_esp_hal_ws2812::buffer_size;
17//! const N: usize = buffer_size(8); // 8-LED ring
18//! ```
19//!
20//! # RMT Clock Configuration
21//!
22//! Configure the RMT channel with [`RMT_CLK_DIV`] to achieve the required 10 MHz clock.
23//! Using a different divider will produce incorrect LED timing.
24//!
25//! # Blocking Example
26//!
27//! ```ignore
28//! use esp_hal::{
29//! gpio::Level,
30//! rmt::{Rmt, TxChannelConfig, TxChannelCreator},
31//! time::Rate,
32//! };
33//! use rgb::RGB8;
34//! use rustyfarian_esp_hal_ws2812::{Ws2812Rmt, buffer_size, RMT_CLK_DIV};
35//!
36//! const N: usize = buffer_size(1);
37//!
38//! let rmt = Rmt::new(peripherals.RMT, Rate::from_mhz(80)).unwrap();
39//! let config = TxChannelConfig::default()
40//! .with_clk_divider(RMT_CLK_DIV)
41//! .with_idle_output_level(Level::Low)
42//! .with_idle_output(true)
43//! .with_carrier_modulation(false);
44//! let channel = rmt.channel0.configure_tx(&config).unwrap().with_pin(peripherals.GPIO8);
45//!
46//! let mut led = Ws2812Rmt::<_, N>::new(channel);
47//! led.set_pixel(RGB8::new(255, 0, 0)).unwrap();
48//!
49//! let colors = [RGB8::new(255, 0, 0), RGB8::new(0, 255, 0), RGB8::new(0, 0, 255)];
50//! led.set_pixels_slice(&colors).unwrap();
51//! ```
52//!
53//! # Async Example (feature `async`)
54//!
55//! ```ignore
56//! use embassy_time::Timer;
57//! use esp_hal::{
58//! gpio::Level,
59//! rmt::{Rmt, TxChannelConfig, TxChannelCreator},
60//! time::Rate,
61//! };
62//! use rgb::RGB8;
63//! use rustyfarian_esp_hal_ws2812::{Ws2812Rmt, buffer_size, RMT_CLK_DIV};
64//!
65//! const NUM_LEDS: usize = 12;
66//! const N: usize = buffer_size(NUM_LEDS);
67//!
68//! let rmt = Rmt::new(peripherals.RMT, Rate::from_mhz(80))
69//! .unwrap()
70//! .into_async();
71//! let config = TxChannelConfig::default()
72//! .with_clk_divider(RMT_CLK_DIV)
73//! .with_idle_output_level(Level::Low)
74//! .with_idle_output(true)
75//! .with_carrier_modulation(false);
76//! let channel = rmt.channel0.configure_tx(&config).unwrap().with_pin(peripherals.GPIO18);
77//!
78//! let mut ws = Ws2812Rmt::<_, N>::new(channel);
79//! let colors = [RGB8::new(255, 0, 0); NUM_LEDS];
80//!
81//! loop {
82//! ws.set_pixels_slice(&colors).await.unwrap();
83//! Timer::after_millis(30).await;
84//! }
85//! ```
86//!
87//! # When does async help?
88//!
89//! WS2812 transmission is fast: approximately 30 µs per LED, or ~360 µs for a 12-LED ring.
90//! In a bare-metal system with no RTOS threads, even that small window matters —
91//! a blocking transmit prevents the executor from servicing any other tasks during that time.
92//!
93//! The larger gain comes from inter-frame delays.
94//! A typical animation loop waits 16–50 ms between frames.
95//! With a blocking `delay_ms()`, the CPU is spinning the whole time.
96//! With `Timer::after_millis(16).await`, the executor is free to handle Wi-Fi events,
97//! button presses, sensor reads, or any other spawned task during that delay.
98//!
99//! # Migration from the pre-0.4 API
100//!
101//! Before `async` support was added, `Ws2812Rmt` had two type parameters: `<'d, N>`.
102//! It now has three: `<'d, Dm, N>` where `Dm` is the driver mode (`Blocking` or `Async`).
103//!
104//! | Before | After |
105//! |:-------|:------|
106//! | `Ws2812Rmt<'d, N>` | `Ws2812Rmt<'d, Blocking, N>` or [`Ws2812RmtBlocking<'d, N>`](Ws2812RmtBlocking) |
107//! | `Ws2812Rmt::<N>::new(channel)` | `Ws2812Rmt::<_, N>::new(channel)` (infers `Blocking` from channel type) |
108//!
109//! The simplest migration is to use the [`Ws2812RmtBlocking`] type alias — no other code changes
110//! are required:
111//!
112//! ```ignore
113//! use rustyfarian_esp_hal_ws2812::{Ws2812RmtBlocking, buffer_size, RMT_CLK_DIV};
114//!
115//! const N: usize = buffer_size(12);
116//! let mut led: Ws2812RmtBlocking<N> = Ws2812RmtBlocking::new(channel);
117//! ```
118//!
119//! Alternatively, let the compiler infer the driver mode:
120//!
121//! ```ignore
122//! let mut led = Ws2812Rmt::<_, N>::new(channel); // Dm inferred from channel type
123//! ```
124//!
125//! # Future: `SmartLedsWriteAsync`
126//!
127//! The `smart-leds-trait` ecosystem defines a `SmartLedsWriteAsync` trait for async LED writers.
128//! Implementing it on `Ws2812Rmt<'d, Async, N>` is a planned follow-on once the trait
129//! stabilises in the ecosystem.
130//! See ADR 006 for details.
131//!
132//! # Other async runtimes
133//!
134//! This crate's async support is built on `esp-hal`'s native async RMT channel and the
135//! `esp-rtos` Embassy executor, which is the standard async runtime for `esp-hal 1.0+`.
136//! Other Embassy-compatible executors (e.g., `embassy-executor` with a custom time driver)
137//! are theoretically possible but untested; the `RmtTxFuture` in `esp-hal` is executor-agnostic
138//! (it uses `core::task::Waker`), but the `embassy-time` timer support requires the
139//! `esp-rtos` time driver to be initialised via `esp_rtos::start()`.
140//!
141//! `esp-idf-hal` (the std path) does not have async RMT support as of `esp-idf-hal 0.46`.
142//! `rustyfarian-esp-idf-ws2812` therefore remains blocking-only.
143//! If `esp-idf-hal` gains async RMT in a future release, async support can be added there
144//! under a separate feature flag without affecting this crate.
145
146use bunting::rgb_to_grb;
147#[cfg(feature = "async")]
148use esp_hal::Async;
149use esp_hal::{
150 gpio::Level,
151 rmt::{Channel, PulseCode, Tx},
152 Blocking,
153};
154use rgb::RGB8;
155use smart_leds_trait::SmartLedsWrite;
156
157/// Clock divider for the RMT peripheral to achieve the required 10 MHz timing clock.
158///
159/// At 80 MHz base clock, divider 8 yields 10 MHz (100 ns per tick).
160/// Pass this constant to [`TxChannelConfig::with_clk_divider`] when constructing the channel.
161pub const RMT_CLK_DIV: u8 = 8;
162
163// WS2812 timing constants at 10 MHz RMT clock (100 ns per tick).
164// Based on WS2812B datasheet typical values.
165const T0H: u16 = 4; // ~400 ns (spec: 350 ns ± 150 ns)
166const T0L: u16 = 8; // ~800 ns (spec: 800 ns ± 150 ns)
167const T1H: u16 = 7; // ~700 ns (spec: 700 ns ± 150 ns)
168const T1L: u16 = 6; // ~600 ns (spec: 600 ns ± 150 ns)
169
170/// Returns the required buffer size (in [`PulseCode`]s) for `num_leds` WS2812 LEDs.
171///
172/// Formula: `num_leds * 24 + 1` — 24 bits of color data per LED, plus one end-of-stream marker.
173///
174/// Use this as the const generic `N` for [`Ws2812Rmt`]:
175///
176/// ```
177/// use rustyfarian_esp_hal_ws2812::buffer_size;
178/// const N: usize = buffer_size(8); // 8-LED ring → 193
179/// assert_eq!(N, 193);
180/// ```
181pub const fn buffer_size(num_leds: usize) -> usize {
182 num_leds * 24 + 1
183}
184
185/// Errors that can occur during WS2812 RMT operations.
186///
187/// # Error recovery
188///
189/// **Blocking** ([`Ws2812Rmt<'d, Blocking, N>`](Ws2812Rmt)):
190/// - If `Channel::transmit()` fails, the channel is consumed and unrecoverable; the driver must be dropped and re-created.
191/// - If `SingleShotTxTransaction::wait()` fails, the channel is returned; the driver is immediately reusable.
192///
193/// **Async** ([`Ws2812Rmt<'d, Async, N>`](Ws2812Rmt)) — `Channel::transmit()` takes `&mut self`
194/// (does not consume the channel), so the driver remains fully usable after [`Error::Transmit`].
195///
196/// [`Error::BufferTooSmall`] is always recoverable in both modes: the buffer is never written
197/// and no transmission is attempted.
198#[derive(Debug, Clone, PartialEq, Eq)]
199pub enum Error {
200 /// RMT peripheral configuration failed.
201 ///
202 /// This variant is reserved for future constructors that configure the RMT peripheral
203 /// internally.
204 RmtConfig,
205 /// RMT transmission failed or (blocking mode only) the channel was lost after a previous
206 /// unrecoverable error.
207 ///
208 /// **Blocking mode**: if `transmit()` fails internally, the `Channel` is consumed by
209 /// `esp-hal` and cannot be recovered.
210 /// Every subsequent call on the same driver instance will also return `Transmit`.
211 /// Recreate the driver from a new channel.
212 ///
213 /// **Async mode**: the channel is never consumed, so the driver is immediately reusable
214 /// after this error.
215 /// The failure typically indicates a hardware-level RMT error (very rare).
216 Transmit,
217 /// The pixel count exceeds the buffer capacity `N`.
218 ///
219 /// Returned synchronously, before any transmission begins (and before any `.await`
220 /// in async mode).
221 /// The buffer is not modified and the channel remains fully operational.
222 ///
223 /// Ensure `N >= num_leds * 24 + 1`.
224 /// Use [`buffer_size`] to compute the correct `N` at compile time.
225 BufferTooSmall,
226}
227
228impl core::fmt::Display for Error {
229 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
230 match self {
231 Error::RmtConfig => write!(f, "RMT peripheral configuration failed"),
232 Error::Transmit => write!(f, "RMT transmission failed"),
233 Error::BufferTooSmall => write!(f, "pixel count exceeds buffer capacity"),
234 }
235 }
236}
237
238/// WS2812 LED driver using the `esp-hal` RMT peripheral (bare-metal, `no_std`).
239///
240/// `N` is the pulse-code buffer size in [`PulseCode`] entries.
241/// Compute it with [`buffer_size`]: `N = num_leds * 24 + 1`.
242///
243/// # Type Parameters
244///
245/// - `'d` — lifetime of the underlying RMT channel.
246/// - `Dm` — driver mode: [`esp_hal::Blocking`] or (with feature `async`) [`esp_hal::Async`].
247/// - `N` — pulse-code buffer size (`num_leds * 24 + 1`).
248///
249/// Use [`Ws2812RmtBlocking`] as a convenience alias for the blocking variant.
250///
251/// # Timing
252///
253/// The driver expects the RMT channel to be configured at 10 MHz
254/// (80 MHz base clock ÷ [`RMT_CLK_DIV`] = 8).
255pub struct Ws2812Rmt<'d, Dm: esp_hal::DriverMode, const N: usize> {
256 /// The RMT TX channel, wrapped in `Option` to support esp-hal's type-state transmit API
257 /// (blocking transmit consumes the channel; wait returns it).
258 channel: Option<Channel<'d, Dm, Tx>>,
259 /// Pre-allocated pulse-code buffer to avoid runtime allocation.
260 buffer: [PulseCode; N],
261}
262
263/// Type alias for the blocking variant of [`Ws2812Rmt`].
264///
265/// Existing code that used `Ws2812Rmt<'d, N>` can migrate to this alias
266/// without further changes.
267pub type Ws2812RmtBlocking<'d, const N: usize> = Ws2812Rmt<'d, Blocking, N>;
268
269/// Shared methods available in both blocking and async modes.
270impl<'d, Dm: esp_hal::DriverMode, const N: usize> Ws2812Rmt<'d, Dm, N> {
271 /// Creates a new WS2812 driver from a pre-configured RMT TX channel.
272 ///
273 /// The channel **must** be configured with [`RMT_CLK_DIV`] (8) on an 80 MHz base clock.
274 /// Using a different clock divider will produce incorrect WS2812 timing.
275 ///
276 /// # Example
277 ///
278 /// ```ignore
279 /// use esp_hal::{
280 /// gpio::Level,
281 /// rmt::{Rmt, TxChannelConfig, TxChannelCreator},
282 /// time::Rate,
283 /// };
284 /// use rustyfarian_esp_hal_ws2812::{Ws2812Rmt, buffer_size, RMT_CLK_DIV};
285 ///
286 /// const N: usize = buffer_size(1);
287 ///
288 /// let rmt = Rmt::new(peripherals.RMT, Rate::from_mhz(80)).unwrap();
289 /// let config = TxChannelConfig::default()
290 /// .with_clk_divider(RMT_CLK_DIV)
291 /// .with_idle_output_level(Level::Low)
292 /// .with_idle_output(true)
293 /// .with_carrier_modulation(false);
294 /// let channel = rmt.channel0.configure_tx(&config).unwrap().with_pin(peripherals.GPIO8);
295 ///
296 /// let mut led = Ws2812Rmt::<_, N>::new(channel);
297 /// ```
298 pub fn new(channel: Channel<'d, Dm, Tx>) -> Self {
299 Self {
300 channel: Some(channel),
301 buffer: [PulseCode::end_marker(); N],
302 }
303 }
304
305 /// Encodes one RGB pixel into 24 consecutive [`PulseCode`] slots (GRB bit order, MSB first).
306 fn encode_color(rgb: RGB8, buf: &mut [PulseCode]) {
307 let grb = rgb_to_grb(rgb);
308 debug_assert_eq!(buf.len(), 24);
309 for (i, slot) in buf.iter_mut().enumerate() {
310 let bit = (grb >> (23 - i)) & 1 != 0;
311 *slot = if bit {
312 PulseCode::new(Level::High, T1H, Level::Low, T1L)
313 } else {
314 PulseCode::new(Level::High, T0H, Level::Low, T0L)
315 };
316 }
317 }
318}
319
320/// Blocking methods.
321impl<'d, const N: usize> Ws2812Rmt<'d, Blocking, N> {
322 /// Sets a single LED to the given color.
323 ///
324 /// The buffer size `N` must be at least 25 (`buffer_size(1)`).
325 ///
326 /// Color is transmitted in WS2812 GRB order.
327 ///
328 /// # Errors
329 ///
330 /// - [`Error::BufferTooSmall`] if `N < 25`.
331 /// - [`Error::Transmit`] if the RMT transmission fails or the channel was previously lost.
332 pub fn set_pixel(&mut self, rgb: RGB8) -> Result<(), Error> {
333 if N < 25 {
334 return Err(Error::BufferTooSmall);
335 }
336 Self::encode_color(rgb, &mut self.buffer[..24]);
337 self.buffer[24] = PulseCode::end_marker();
338 self.do_transmit(25)
339 }
340
341 /// Sets multiple LEDs from a color slice.
342 ///
343 /// Colors are transmitted in WS2812 GRB order.
344 /// The buffer size `N` must be at least `rgbs.len() * 24 + 1`.
345 ///
346 /// # Errors
347 ///
348 /// - [`Error::BufferTooSmall`] if `N < rgbs.len() * 24 + 1`.
349 /// - [`Error::Transmit`] if the RMT transmission fails or the channel was previously lost.
350 pub fn set_pixels_slice(&mut self, rgbs: &[RGB8]) -> Result<(), Error> {
351 let num_leds = rgbs.len();
352 let needed = num_leds * 24 + 1;
353 if needed > N {
354 return Err(Error::BufferTooSmall);
355 }
356 for (i, &rgb) in rgbs.iter().enumerate() {
357 Self::encode_color(rgb, &mut self.buffer[i * 24..(i + 1) * 24]);
358 }
359 self.buffer[num_leds * 24] = PulseCode::end_marker();
360 self.do_transmit(needed)
361 }
362
363 /// Sends `buffer[..len]` via the RMT channel and waits for completion.
364 ///
365 /// Uses `Option<Channel>` to handle esp-hal's ownership-based transmit API:
366 /// `transmit()` consumes the channel and `wait()` returns it.
367 fn do_transmit(&mut self, len: usize) -> Result<(), Error> {
368 let ch = self.channel.take().ok_or(Error::Transmit)?;
369 // transmit() consumes `ch`; on Err the channel is unrecoverable
370 let txn = ch
371 .transmit(&self.buffer[..len])
372 .map_err(|_| Error::Transmit)?;
373 // wait() consumes `txn`, releasing the borrow on `self.buffer`
374 match txn.wait() {
375 Ok(ch_back) => {
376 self.channel = Some(ch_back);
377 Ok(())
378 }
379 Err((_, ch_back)) => {
380 self.channel = Some(ch_back);
381 Err(Error::Transmit)
382 }
383 }
384 }
385}
386
387/// Async methods (requires feature `async`).
388///
389/// Unlike the blocking variant, the async `Channel::transmit()` takes `&mut self` and returns
390/// a `Future` directly — no channel ownership transfer occurs.
391#[cfg(feature = "async")]
392impl<'d, const N: usize> Ws2812Rmt<'d, Async, N> {
393 /// Sets a single LED to the given color, yielding to the executor during transmission.
394 ///
395 /// The buffer size `N` must be at least 25 (`buffer_size(1)`).
396 ///
397 /// Color is transmitted in WS2812 GRB order.
398 ///
399 /// # Errors
400 ///
401 /// - [`Error::BufferTooSmall`] — returned immediately, **before** any `.await`, if `N < 25`.
402 /// The buffer is not modified and the driver remains fully operational.
403 /// - [`Error::Transmit`] — returned after `.await` completes if the RMT hardware signals
404 /// an error.
405 /// Because the async channel is never consumed, the driver is reusable after this error.
406 ///
407 /// # Example
408 ///
409 /// ```ignore
410 /// match ws.set_pixel(RGB8::new(255, 0, 0)).await {
411 /// Ok(()) => {}
412 /// Err(Error::BufferTooSmall) => {
413 /// // N is too small for even one LED — fix the const N at compile time.
414 /// panic!("buffer too small");
415 /// }
416 /// Err(Error::Transmit) => {
417 /// // Hardware error — driver is still usable; retry or log.
418 /// log_error();
419 /// }
420 /// Err(_) => unreachable!(),
421 /// }
422 /// ```
423 pub async fn set_pixel(&mut self, rgb: RGB8) -> Result<(), Error> {
424 if N < 25 {
425 return Err(Error::BufferTooSmall);
426 }
427 Self::encode_color(rgb, &mut self.buffer[..24]);
428 self.buffer[24] = PulseCode::end_marker();
429 self.do_transmit_async(25).await
430 }
431
432 /// Sets multiple LEDs from a color slice, yielding to the executor during transmission.
433 ///
434 /// Colors are transmitted in WS2812 GRB order.
435 /// The buffer size `N` must be at least `rgbs.len() * 24 + 1`.
436 ///
437 /// # Errors
438 ///
439 /// - [`Error::BufferTooSmall`] — returned immediately, **before** any `.await`, if
440 /// `N < rgbs.len() * 24 + 1`.
441 /// No data is written to the buffer and the channel remains fully operational.
442 /// Fix: use [`buffer_size`]`(num_leds)` to size `N` correctly at compile time,
443 /// or ensure the slice length does not exceed `(N - 1) / 24`.
444 /// - [`Error::Transmit`] — returned after `.await` completes if the RMT hardware signals
445 /// an error (very rare).
446 /// Because the async channel is never consumed, the driver is reusable after this error.
447 ///
448 /// # Rapid consecutive calls
449 ///
450 /// The `transmit().await` future completes only after the RMT peripheral finishes sending
451 /// all pulses.
452 /// Awaiting completion before calling again is therefore the natural backpressure mechanism —
453 /// no explicit queuing is needed.
454 /// If you call `set_pixels_slice` in a tight loop without an inter-frame delay,
455 /// the executor will context-switch to other tasks during each transmission
456 /// (~30 µs for a 12-LED ring), then resume for the next frame.
457 ///
458 /// # Example
459 ///
460 /// ```ignore
461 /// let colors = [RGB8::new(255, 0, 0); 12];
462 /// match ws.set_pixels_slice(&colors).await {
463 /// Ok(()) => {}
464 /// Err(Error::BufferTooSmall) => {
465 /// // Slice is longer than N can hold — fix N or shorten the slice.
466 /// panic!("buffer too small: N={N}, needed={}", colors.len() * 24 + 1);
467 /// }
468 /// Err(Error::Transmit) => {
469 /// // Hardware error — driver is still usable; retry or log.
470 /// log_error();
471 /// }
472 /// Err(_) => unreachable!(),
473 /// }
474 /// ```
475 pub async fn set_pixels_slice(&mut self, rgbs: &[RGB8]) -> Result<(), Error> {
476 let num_leds = rgbs.len();
477 let needed = num_leds * 24 + 1;
478 if needed > N {
479 return Err(Error::BufferTooSmall);
480 }
481 for (i, &rgb) in rgbs.iter().enumerate() {
482 Self::encode_color(rgb, &mut self.buffer[i * 24..(i + 1) * 24]);
483 }
484 self.buffer[num_leds * 24] = PulseCode::end_marker();
485 self.do_transmit_async(needed).await
486 }
487
488 /// Sends `buffer[..len]` via the async RMT channel, awaiting completion.
489 ///
490 /// The async `Channel::transmit()` takes `&mut self` (does not consume the channel),
491 /// so no `Option` dance is needed here.
492 async fn do_transmit_async(&mut self, len: usize) -> Result<(), Error> {
493 let ch = self.channel.as_mut().ok_or(Error::Transmit)?;
494 ch.transmit(&self.buffer[..len])
495 .await
496 .map_err(|_| Error::Transmit)
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 extern crate std;
503
504 use super::*;
505 use std::string::ToString;
506
507 // --- buffer_size tests ---------------------------------------------------
508
509 #[test]
510 fn buffer_size_formula() {
511 // Verify the documented formula: num_leds * 24 + 1
512 // Includes edge cases: 0 LEDs (end-marker only), common ring sizes.
513 for n in [0usize, 1, 4, 8, 12, 16, 60] {
514 assert_eq!(buffer_size(n), n * 24 + 1, "buffer_size({n}) mismatch");
515 }
516 }
517
518 // --- max_leds capacity edge case -----------------------------------------
519
520 #[test]
521 fn max_leds_formula_n1_yields_zero() {
522 // N=1: only the end-of-stream marker fits; no LED data can be stored.
523 let max = (1usize.saturating_sub(1)) / 24;
524 assert_eq!(max, 0);
525 }
526
527 // --- Error Display tests -------------------------------------------------
528
529 #[test]
530 fn error_display_rmt_config_message() {
531 assert_eq!(
532 Error::RmtConfig.to_string(),
533 "RMT peripheral configuration failed"
534 );
535 }
536
537 #[test]
538 fn error_display_transmit_message() {
539 assert_eq!(Error::Transmit.to_string(), "RMT transmission failed");
540 }
541
542 #[test]
543 fn error_display_buffer_too_small_message() {
544 assert_eq!(
545 Error::BufferTooSmall.to_string(),
546 "pixel count exceeds buffer capacity"
547 );
548 }
549
550 // --- Error derive trait tests --------------------------------------------
551 //
552 // These verify the PartialEq, Clone, and Debug derives, which matter for
553 // callers that match on or log errors — including async callers where the
554 // error is returned from an .await expression.
555
556 #[test]
557 fn error_partial_eq_same_variants() {
558 assert_eq!(Error::BufferTooSmall, Error::BufferTooSmall);
559 assert_eq!(Error::Transmit, Error::Transmit);
560 assert_eq!(Error::RmtConfig, Error::RmtConfig);
561 }
562
563 #[test]
564 fn error_partial_eq_different_variants() {
565 assert_ne!(Error::BufferTooSmall, Error::Transmit);
566 assert_ne!(Error::Transmit, Error::RmtConfig);
567 assert_ne!(Error::BufferTooSmall, Error::RmtConfig);
568 }
569
570 #[test]
571 fn error_clone_produces_equal_value() {
572 assert_eq!(Error::BufferTooSmall.clone(), Error::BufferTooSmall);
573 assert_eq!(Error::Transmit.clone(), Error::Transmit);
574 assert_eq!(Error::RmtConfig.clone(), Error::RmtConfig);
575 }
576
577 #[test]
578 fn error_debug_contains_variant_name() {
579 // Debug output is used when logging errors from async tasks; verify
580 // each variant formats recognisably.
581 let s = std::format!("{:?}", Error::BufferTooSmall);
582 assert!(s.contains("BufferTooSmall"), "got: {s}");
583
584 let s = std::format!("{:?}", Error::Transmit);
585 assert!(s.contains("Transmit"), "got: {s}");
586
587 let s = std::format!("{:?}", Error::RmtConfig);
588 assert!(s.contains("RmtConfig"), "got: {s}");
589 }
590
591 // --- BufferTooSmall guard boundary tests ---------------------------------
592 //
593 // The guard `needed > N` is evaluated synchronously, before any hardware
594 // interaction (and before any .await in async mode).
595 // Verify the boundary arithmetic that drives it.
596
597 #[test]
598 fn buffer_too_small_boundary_single_led() {
599 // buffer_size(1) = 25; N=24 is one slot short.
600 let needed_for_one: usize = 1 * 24 + 1; // 25
601 assert!(needed_for_one > 24, "N=24 must trigger BufferTooSmall");
602 assert!(
603 !(needed_for_one > 25),
604 "N=25 must NOT trigger BufferTooSmall"
605 );
606 }
607
608 #[test]
609 fn buffer_too_small_boundary_twelve_leds() {
610 // buffer_size(12) = 289; N=288 is one slot short.
611 let needed: usize = 12 * 24 + 1; // 289
612 assert!(needed > 288, "N=288 must trigger BufferTooSmall");
613 assert!(!(needed > 289), "N=289 must NOT trigger BufferTooSmall");
614 }
615
616 #[test]
617 fn buffer_too_small_empty_slice_never_triggers() {
618 // Empty slice: needed = 0 * 24 + 1 = 1; fits in any buffer (min N=1 for end marker).
619 let needed: usize = 0 * 24 + 1; // 1
620 assert!(
621 !(needed > 1),
622 "empty slice must never trigger BufferTooSmall"
623 );
624 }
625}
626
627#[cfg(feature = "pennant")]
628impl<'d, const N: usize> pennant::StatusLed for Ws2812Rmt<'d, Blocking, N> {
629 type Error = Error;
630
631 fn set_color(&mut self, color: RGB8) -> Result<(), Self::Error> {
632 self.set_pixel(color)
633 }
634}
635
636/// `SmartLedsWrite` implementation for [`Ws2812Rmt`].
637///
638/// Allows the driver to be used with any crate in the `smart-leds` ecosystem
639/// (e.g. `smart-leds`, brightness adapters, gamma correction).
640///
641/// The iterator is drained directly into the pre-allocated pulse-code buffer —
642/// no heap allocation occurs. If the iterator yields more colors than the buffer
643/// can hold (`(N - 1) / 24` LEDs), transmission is aborted and
644/// [`Error::BufferTooSmall`] is returned before any data is sent.
645///
646/// If the iterator is empty, `Ok(())` is returned immediately — no reset pulse
647/// or blank frame is sent. Hardware that requires an explicit blank to turn off
648/// LEDs should send a zeroed color slice instead.
649///
650/// # Example
651///
652/// ```ignore
653/// use smart_leds_trait::{SmartLedsWrite, RGB8};
654///
655/// let colors = [RGB8 { r: 255, g: 0, b: 0 }; 8];
656/// led.write(colors.iter().cloned()).unwrap();
657/// ```
658impl<'d, const N: usize> SmartLedsWrite for Ws2812Rmt<'d, Blocking, N> {
659 type Error = Error;
660 type Color = smart_leds_trait::RGB8;
661
662 fn write<T, I>(&mut self, iterator: T) -> Result<(), Self::Error>
663 where
664 T: IntoIterator<Item = I>,
665 I: Into<smart_leds_trait::RGB8>,
666 {
667 let max_leds = (N.saturating_sub(1)) / 24;
668 let mut num_leds = 0usize;
669
670 for item in iterator {
671 if num_leds >= max_leds {
672 return Err(Error::BufferTooSmall);
673 }
674 let rgb: RGB8 = item.into();
675 let start = num_leds * 24;
676 Self::encode_color(rgb, &mut self.buffer[start..start + 24]);
677 num_leds += 1;
678 }
679
680 if num_leds == 0 {
681 return Ok(());
682 }
683
684 self.buffer[num_leds * 24] = PulseCode::end_marker();
685 self.do_transmit(num_leds * 24 + 1)
686 }
687}
688
689#[cfg(all(feature = "async", feature = "pennant"))]
690#[allow(async_fn_in_trait)]
691impl<'d, const N: usize> pennant::AsyncStatusLed for Ws2812Rmt<'d, Async, N> {
692 type Error = Error;
693
694 async fn set_color(&mut self, color: RGB8) -> Result<(), Self::Error> {
695 self.set_pixel(color).await
696 }
697}