Skip to main content

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}