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}