unicorn_hat/
lib.rs

1//! Rust library for controlling the Pimoroni Unicorn HAT (8x8) on Raspberry Pi
2//!
3//! This library provides a safe, ergonomic API for controlling the 8x8 WS2812 LED grid
4//! on the Pimoroni Unicorn HAT.
5//!
6//! # Hardware Requirements
7//!
8//! - Pimoroni Unicorn HAT (8x8, NOT the HD version)
9//! - Raspberry Pi with GPIO access
10//! - SPI enabled via raspi-config
11//! - Root/sudo privileges (required for PWM hardware access)
12//!
13//! # Quick Start
14//!
15//! ```no_run
16//! use unicorn_hat::{UnicornHat, RGB8};
17//!
18//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
19//! // Initialize the HAT
20//! let mut hat = UnicornHat::new()?;
21//!
22//! // Set a pixel and display
23//! hat.set_pixel_raw(0, RGB8::RED)?;
24//! hat.display()?;
25//!
26//! // Clear all pixels
27//! hat.clear();
28//! hat.display()?;
29//! # Ok(())
30//! # }
31//! ```
32
33use rs_ws281x::{ChannelBuilder, Controller, ControllerBuilder, StripType, WS2811Error};
34use thiserror::Error;
35
36pub mod buffer;
37mod color;
38mod mapper;
39pub mod primitives;
40mod rotation;
41
42pub use buffer::{PixelBuffer, TestBuffer};
43pub use color::{Palette, RGB8, HSV};
44pub use mapper::{PixelMapper, ZigzagMapping};
45pub use rotation::{Origin, Rotate};
46
47/// Hardware configuration constants
48const GPIO_PIN: i32 = 18;
49const DMA_CHANNEL: i32 = 10;
50const LED_COUNT: usize = 64;
51const FREQUENCY: u32 = 800_000; // 800kHz for WS2812
52
53/// Errors that can occur when using the Unicorn HAT
54#[derive(Error, Debug)]
55pub enum Error {
56    /// Invalid pixel index (must be 0-63 for 8x8 grid)
57    #[error("invalid pixel index {0}, must be 0-63")]
58    InvalidIndex(usize),
59
60    /// Invalid coordinate (x or y must be 0-7)
61    #[error("invalid coordinate ({0}, {1}), x and y must be 0-7")]
62    InvalidCoordinate(usize, usize),
63
64    /// Hardware error from the WS2812 controller
65    #[error("hardware error: {0}")]
66    Hardware(#[from] WS2811Error),
67}
68
69/// Main interface for controlling the Unicorn HAT
70///
71/// This struct provides low-level access to the 64 LEDs on the Unicorn HAT.
72/// Pixels are addressed by raw index (0-63) in this core implementation.
73///
74/// # Examples
75///
76/// ```no_run
77/// use unicorn_hat::{UnicornHat, RGB8};
78///
79/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
80/// let mut hat = UnicornHat::new()?;
81///
82/// // Light up first LED in red
83/// hat.set_pixel_raw(0, RGB8::RED)?;
84/// hat.display()?;
85///
86/// // Clear all
87/// hat.clear();
88/// hat.display()?;
89/// # Ok(())
90/// # }
91/// ```
92pub struct UnicornHat {
93    controller: Controller,
94    buffer: [RGB8; LED_COUNT],
95    mapper: ZigzagMapping,
96    rotation: Rotate,
97    origin: Origin,
98    brightness: u8,
99}
100
101impl UnicornHat {
102    /// Creates a new UnicornHat instance and initializes the hardware
103    ///
104    /// This will initialize the WS2812 controller with the correct GPIO pin (18),
105    /// DMA channel (10), and LED count (64).
106    ///
107    /// # Errors
108    ///
109    /// Returns an error if:
110    /// - Hardware initialization fails
111    /// - Insufficient permissions (must run with sudo)
112    /// - PWM hardware is in use by another process
113    ///
114    /// # Examples
115    ///
116    /// ```no_run
117    /// use unicorn_hat::UnicornHat;
118    ///
119    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
120    /// let mut hat = UnicornHat::new()?;
121    /// # Ok(())
122    /// # }
123    /// ```
124    pub fn new() -> Result<Self, Error> {
125        let controller = ControllerBuilder::new()
126            .freq(FREQUENCY)
127            .dma(DMA_CHANNEL)
128            .channel(
129                0,
130                ChannelBuilder::new()
131                    .pin(GPIO_PIN)
132                    .count(LED_COUNT as i32)
133                    .strip_type(StripType::Ws2812)
134                    .brightness(255)
135                    .build(),
136            )
137            .build()?;
138
139        Ok(UnicornHat {
140            controller,
141            buffer: [RGB8::BLACK; LED_COUNT],
142            mapper: ZigzagMapping,
143            rotation: Rotate::default(),
144            origin: Origin::default(),
145            brightness: 128, // 50% default brightness
146        })
147    }
148
149    /// Sets a pixel color by raw index
150    ///
151    /// The index ranges from 0 to 63 for the 8x8 grid. This is a low-level method
152    /// that directly addresses the LED buffer index without coordinate transformation.
153    ///
154    /// Note: Changes are not visible until [`display()`](Self::display) is called.
155    ///
156    /// # Errors
157    ///
158    /// Returns [`Error::InvalidIndex`] if the index is greater than 63.
159    ///
160    /// # Examples
161    ///
162    /// ```no_run
163    /// use unicorn_hat::{UnicornHat, RGB8};
164    ///
165    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
166    /// let mut hat = UnicornHat::new()?;
167    ///
168    /// // Set first LED to red
169    /// hat.set_pixel_raw(0, RGB8::RED)?;
170    ///
171    /// // Set last LED to blue
172    /// hat.set_pixel_raw(63, RGB8::BLUE)?;
173    ///
174    /// hat.display()?;
175    /// # Ok(())
176    /// # }
177    /// ```
178    pub fn set_pixel_raw(&mut self, index: usize, color: RGB8) -> Result<(), Error> {
179        if index >= LED_COUNT {
180            return Err(Error::InvalidIndex(index));
181        }
182        self.buffer[index] = color;
183        Ok(())
184    }
185
186    /// Gets the color of a pixel by raw index
187    ///
188    /// Returns the current color in the buffer (not necessarily what's displayed
189    /// on the hardware if [`display()`](Self::display) hasn't been called).
190    ///
191    /// # Errors
192    ///
193    /// Returns [`Error::InvalidIndex`] if the index is greater than 63.
194    ///
195    /// # Examples
196    ///
197    /// ```no_run
198    /// use unicorn_hat::{UnicornHat, RGB8};
199    ///
200    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
201    /// let mut hat = UnicornHat::new()?;
202    /// hat.set_pixel_raw(0, RGB8::RED)?;
203    ///
204    /// let color = hat.get_pixel_raw(0)?;
205    /// assert_eq!(color, RGB8::RED);
206    /// # Ok(())
207    /// # }
208    /// ```
209    pub fn get_pixel_raw(&self, index: usize) -> Result<RGB8, Error> {
210        if index >= LED_COUNT {
211            return Err(Error::InvalidIndex(index));
212        }
213        Ok(self.buffer[index])
214    }
215
216    /// Renders the current buffer to the physical LEDs
217    ///
218    /// All changes made via [`set_pixel_raw()`](Self::set_pixel_raw) or
219    /// [`clear()`](Self::clear) are not visible until this method is called.
220    ///
221    /// # Errors
222    ///
223    /// Returns an error if the hardware render operation fails.
224    ///
225    /// # Examples
226    ///
227    /// ```no_run
228    /// use unicorn_hat::{UnicornHat, RGB8};
229    ///
230    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
231    /// let mut hat = UnicornHat::new()?;
232    ///
233    /// hat.set_pixel_raw(0, RGB8::GREEN)?;
234    /// hat.display()?; // Now the LED is actually green
235    /// # Ok(())
236    /// # }
237    /// ```
238    pub fn display(&mut self) -> Result<(), Error> {
239        // Copy buffer to controller in BGR format with brightness scaling
240        let brightness_factor = self.brightness as u16;
241
242        for (i, color) in self.buffer.iter().enumerate() {
243            // Apply brightness scaling to each component
244            let scaled = RGB8::new(
245                ((color.r as u16 * brightness_factor) / 255) as u8,
246                ((color.g as u16 * brightness_factor) / 255) as u8,
247                ((color.b as u16 * brightness_factor) / 255) as u8,
248            );
249            self.controller.leds_mut(0)[i] = scaled.to_hardware_format();
250        }
251
252        // Render to hardware
253        self.controller.render()?;
254        Ok(())
255    }
256
257    /// Clears all pixels to black
258    ///
259    /// Sets all pixels in the buffer to black (RGB8::BLACK).
260    /// Note: Changes are not visible until [`display()`](Self::display) is called.
261    ///
262    /// # Examples
263    ///
264    /// ```no_run
265    /// use unicorn_hat::UnicornHat;
266    ///
267    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
268    /// let mut hat = UnicornHat::new()?;
269    ///
270    /// // Turn off all LEDs
271    /// hat.clear();
272    /// hat.display()?;
273    /// # Ok(())
274    /// # }
275    /// ```
276    pub fn clear(&mut self) {
277        self.buffer.fill(RGB8::BLACK);
278    }
279
280    /// Sets a pixel color using (x, y) coordinates
281    ///
282    /// Coordinates are in the range 0-7, where (0, 0) is top-left and (7, 7) is bottom-right.
283    /// Takes rotation into account - coordinates are relative to the current rotation setting.
284    ///
285    /// Note: Changes are not visible until [`display()`](Self::display) is called.
286    ///
287    /// # Errors
288    ///
289    /// Returns [`Error::InvalidCoordinate`] if x or y is greater than 7.
290    ///
291    /// # Examples
292    ///
293    /// ```no_run
294    /// use unicorn_hat::{UnicornHat, RGB8};
295    ///
296    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
297    /// let mut hat = UnicornHat::new()?;
298    ///
299    /// // Set top-left corner to red
300    /// hat.set_pixel(0, 0, RGB8::RED)?;
301    ///
302    /// // Set bottom-right corner to blue
303    /// hat.set_pixel(7, 7, RGB8::BLUE)?;
304    ///
305    /// hat.display()?;
306    /// # Ok(())
307    /// # }
308    /// ```
309    pub fn set_pixel(&mut self, x: usize, y: usize, color: RGB8) -> Result<(), Error> {
310        if x >= 8 || y >= 8 {
311            return Err(Error::InvalidCoordinate(x, y));
312        }
313
314        // Apply origin transform, then rotation, then mapping
315        let (ox, oy) = self.origin.apply(x, y);
316        let (rx, ry) = self.rotation.apply(ox, oy);
317        let index = self.mapper.coord_to_index(rx, ry);
318
319        self.buffer[index] = color;
320        Ok(())
321    }
322
323    /// Gets the color of a pixel using (x, y) coordinates
324    ///
325    /// Coordinates are in the range 0-7, where (0, 0) is top-left and (7, 7) is bottom-right.
326    /// Takes rotation into account - coordinates are relative to the current rotation setting.
327    ///
328    /// Returns the current color in the buffer (not necessarily what's displayed
329    /// on the hardware if [`display()`](Self::display) hasn't been called).
330    ///
331    /// # Errors
332    ///
333    /// Returns [`Error::InvalidCoordinate`] if x or y is greater than 7.
334    ///
335    /// # Examples
336    ///
337    /// ```no_run
338    /// use unicorn_hat::{UnicornHat, RGB8};
339    ///
340    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
341    /// let mut hat = UnicornHat::new()?;
342    /// hat.set_pixel(0, 0, RGB8::RED)?;
343    ///
344    /// let color = hat.get_pixel(0, 0)?;
345    /// assert_eq!(color, RGB8::RED);
346    /// # Ok(())
347    /// # }
348    /// ```
349    pub fn get_pixel(&self, x: usize, y: usize) -> Result<RGB8, Error> {
350        if x >= 8 || y >= 8 {
351            return Err(Error::InvalidCoordinate(x, y));
352        }
353
354        // Apply origin transform, then rotation, then mapping
355        let (ox, oy) = self.origin.apply(x, y);
356        let (rx, ry) = self.rotation.apply(ox, oy);
357        let index = self.mapper.coord_to_index(rx, ry);
358
359        Ok(self.buffer[index])
360    }
361
362    /// Sets the rotation of the display
363    ///
364    /// Changes how coordinates map to physical LEDs. Useful when the HAT is
365    /// mounted in different orientations.
366    ///
367    /// # Examples
368    ///
369    /// ```no_run
370    /// use unicorn_hat::{UnicornHat, Rotate, RGB8};
371    ///
372    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
373    /// let mut hat = UnicornHat::new()?;
374    ///
375    /// // Rotate 90° clockwise
376    /// hat.set_rotation(Rotate::RotCW90);
377    ///
378    /// // Now (0,0) maps to what was the top-right corner
379    /// hat.set_pixel(0, 0, RGB8::RED)?;
380    /// hat.display()?;
381    /// # Ok(())
382    /// # }
383    /// ```
384    pub fn set_rotation(&mut self, rotation: Rotate) {
385        self.rotation = rotation;
386    }
387
388    /// Gets the current rotation setting
389    ///
390    /// # Examples
391    ///
392    /// ```no_run
393    /// use unicorn_hat::{UnicornHat, Rotate};
394    ///
395    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
396    /// let hat = UnicornHat::new()?;
397    /// assert_eq!(hat.get_rotation(), Rotate::RotNone);
398    /// # Ok(())
399    /// # }
400    /// ```
401    pub fn get_rotation(&self) -> Rotate {
402        self.rotation
403    }
404
405    /// Sets the coordinate system origin
406    ///
407    /// Changes where (0,0) is located on the display. Use `Origin::BottomLeft`
408    /// for bar graphs where Y represents a value that increases upward.
409    ///
410    /// # Examples
411    ///
412    /// ```no_run
413    /// use unicorn_hat::{UnicornHat, Origin, RGB8};
414    ///
415    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
416    /// let mut hat = UnicornHat::new()?;
417    ///
418    /// // Use bottom-left origin for bar graph
419    /// hat.set_origin(Origin::BottomLeft);
420    ///
421    /// // Draw a bar at x=0 with height 5
422    /// for y in 0..5 {
423    ///     hat.set_pixel(0, y, RGB8::GREEN)?;
424    /// }
425    /// hat.display()?;
426    /// # Ok(())
427    /// # }
428    /// ```
429    pub fn set_origin(&mut self, origin: Origin) {
430        self.origin = origin;
431    }
432
433    /// Gets the current coordinate system origin
434    ///
435    /// # Examples
436    ///
437    /// ```no_run
438    /// use unicorn_hat::{UnicornHat, Origin};
439    ///
440    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
441    /// let hat = UnicornHat::new()?;
442    /// assert_eq!(hat.get_origin(), Origin::TopLeft);
443    /// # Ok(())
444    /// # }
445    /// ```
446    pub fn get_origin(&self) -> Origin {
447        self.origin
448    }
449
450    /// Sets the brightness level
451    ///
452    /// Brightness is applied during [`display()`](Self::display) by scaling all RGB values.
453    /// Does not modify the buffer, only affects rendering.
454    ///
455    /// # Arguments
456    ///
457    /// * `brightness` - Brightness level (0-255)
458    ///   - 0 = All LEDs off
459    ///   - 128 = 50% brightness (default)
460    ///   - 255 = Full brightness
461    ///
462    /// # Examples
463    ///
464    /// ```no_run
465    /// use unicorn_hat::{UnicornHat, RGB8};
466    ///
467    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
468    /// let mut hat = UnicornHat::new()?;
469    ///
470    /// // Set to 25% brightness
471    /// hat.set_brightness(64);
472    ///
473    /// hat.set_pixel(0, 0, RGB8::WHITE)?;
474    /// hat.display()?;  // White pixel appears at 25% brightness
475    /// # Ok(())
476    /// # }
477    /// ```
478    pub fn set_brightness(&mut self, brightness: u8) {
479        self.brightness = brightness;
480    }
481
482    /// Gets the current brightness level
483    ///
484    /// # Examples
485    ///
486    /// ```no_run
487    /// use unicorn_hat::UnicornHat;
488    ///
489    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
490    /// let hat = UnicornHat::new()?;
491    /// assert_eq!(hat.get_brightness(), 128);  // Default 50%
492    /// # Ok(())
493    /// # }
494    /// ```
495    pub fn get_brightness(&self) -> u8 {
496        self.brightness
497    }
498
499    /// Fills all pixels with a single color
500    ///
501    /// Sets every pixel in the 8x8 grid to the same color.
502    /// Note: Changes are not visible until [`display()`](Self::display) is called.
503    ///
504    /// # Examples
505    ///
506    /// ```no_run
507    /// use unicorn_hat::{UnicornHat, RGB8};
508    ///
509    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
510    /// let mut hat = UnicornHat::new()?;
511    ///
512    /// // Fill entire display with red
513    /// hat.fill(RGB8::RED);
514    /// hat.display()?;
515    /// # Ok(())
516    /// # }
517    /// ```
518    pub fn fill(&mut self, color: RGB8) {
519        self.buffer.fill(color);
520    }
521
522    /// Sets all pixels from a 2D array
523    ///
524    /// Sets the entire 8x8 grid from a 2D array of colors.
525    /// Array is indexed as `[y][x]` (row-major order).
526    ///
527    /// Note: Changes are not visible until [`display()`](Self::display) is called.
528    ///
529    /// # Examples
530    ///
531    /// ```no_run
532    /// use unicorn_hat::{UnicornHat, RGB8};
533    ///
534    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
535    /// let mut hat = UnicornHat::new()?;
536    ///
537    /// // Create a checkerboard pattern
538    /// let mut grid = [[RGB8::BLACK; 8]; 8];
539    /// for y in 0..8 {
540    ///     for x in 0..8 {
541    ///         if (x + y) % 2 == 0 {
542    ///             grid[y][x] = RGB8::WHITE;
543    ///         }
544    ///     }
545    /// }
546    ///
547    /// hat.set_all(&grid)?;
548    /// hat.display()?;
549    /// # Ok(())
550    /// # }
551    /// ```
552    #[allow(clippy::needless_range_loop)]
553    pub fn set_all(&mut self, grid: &[[RGB8; 8]; 8]) -> Result<(), Error> {
554        for y in 0..8 {
555            for x in 0..8 {
556                self.set_pixel(x, y, grid[y][x])?;
557            }
558        }
559        Ok(())
560    }
561
562    /// Gets all pixels as a 2D array
563    ///
564    /// Returns the current buffer contents as a 2D array.
565    /// Array is indexed as `[y][x]` (row-major order).
566    ///
567    /// # Examples
568    ///
569    /// ```no_run
570    /// use unicorn_hat::{UnicornHat, RGB8};
571    ///
572    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
573    /// let mut hat = UnicornHat::new()?;
574    ///
575    /// hat.set_pixel(3, 3, RGB8::RED)?;
576    ///
577    /// let grid = hat.get_all()?;
578    /// assert_eq!(grid[3][3], RGB8::RED);
579    /// # Ok(())
580    /// # }
581    /// ```
582    #[allow(clippy::needless_range_loop)]
583    pub fn get_all(&self) -> Result<[[RGB8; 8]; 8], Error> {
584        let mut grid = [[RGB8::BLACK; 8]; 8];
585        for y in 0..8 {
586            for x in 0..8 {
587                grid[y][x] = self.get_pixel(x, y)?;
588            }
589        }
590        Ok(grid)
591    }
592
593    /// Shows a frame buffer on the display.
594    ///
595    /// Copies all pixels from a frame buffer to the display and renders them.
596    /// This is useful for double-buffering and smooth animations.
597    ///
598    /// Note: This method calls [`display()`](Self::display) internally, so changes
599    /// are immediately visible on the hardware.
600    ///
601    /// # Examples
602    ///
603    /// ```no_run
604    /// # #[cfg(feature = "extras")]
605    /// # {
606    /// use unicorn_hat::{UnicornHat, RGB8};
607    ///
608    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
609    /// let mut hat = UnicornHat::new()?;
610    ///
611    /// // This example requires the unicorn-hat-extras crate
612    /// # Ok(())
613    /// # }
614    /// # }
615    /// ```
616    pub fn show_frame(&mut self, frame: &[[RGB8; 8]; 8]) -> Result<(), Error> {
617        self.set_all(frame)?;
618        self.display()?;
619        Ok(())
620    }
621}
622
623// Implement PixelBuffer trait for UnicornHat
624impl buffer::PixelBuffer for UnicornHat {
625    fn set_pixel(&mut self, x: usize, y: usize, color: RGB8) -> Result<(), Error> {
626        self.set_pixel(x, y, color)
627    }
628
629    fn get_pixel(&self, x: usize, y: usize) -> Result<RGB8, Error> {
630        self.get_pixel(x, y)
631    }
632}
633
634#[cfg(test)]
635mod tests {
636    use super::*;
637
638    #[test]
639    fn test_error_invalid_index() {
640        let err = Error::InvalidIndex(100);
641        let err_string = format!("{}", err);
642        assert!(err_string.contains("100"));
643        assert!(err_string.contains("0-63"));
644    }
645
646    #[test]
647    fn test_error_invalid_coordinate() {
648        let err = Error::InvalidCoordinate(10, 15);
649        let err_string = format!("{}", err);
650        assert!(err_string.contains("10"));
651        assert!(err_string.contains("15"));
652        assert!(err_string.contains("0-7"));
653    }
654
655    // Note: Hardware tests are in examples/
656    // RGB8 and HSV tests are in src/color.rs
657    // Mapping tests are in src/mapper.rs
658    // Rotation tests are in src/rotation.rs
659}