#!/usr/bin/env python # -*- coding: utf-8 -*- """Unicorn HAT HD library. Drive the 16x16 RGB pixel Pimoronu Unicorn HAT HD over SPI from a Raspberry Pi or compatible platform. """ import colorsys import time try: import spidev except ImportError: raise ImportError('This library requires the spidev module\nInstall with: sudo pip install spidev') try: import numpy except ImportError: raise ImportError('This library requires the numpy module\nInstall with: sudo pip install numpy') __version__ = '0.0.4' _SOF = 0x72 _DELAY = 1.0 / 120 WIDTH = 16 HEIGHT = 16 PHAT = None HAT = None PHAT_VERTICAL = None AUTO = None PANEL_SHAPE = (16, 16) _rotation = 0 _brightness = 0.5 _buffer_width = 16 _buffer_height = 16 _addressing_enabled = False _buf = numpy.zeros((_buffer_width, _buffer_height, 3), dtype=int) COLORS = { 'red': (255, 0, 0), 'lime': (0, 255, 0), 'blue': (0, 0, 255), 'yellow': (255, 255, 0), 'magenta': (255, 0, 255), 'cyan': (0, 255, 255), 'black': (0, 0, 0), 'white': (255, 255, 255), 'gray': (127, 127, 127), 'grey': (127, 127, 127), 'silver': (192, 192, 192), 'maroon': (128, 0, 0), 'olive': (128, 128, 0), 'green': (0, 128, 0), 'purple': (128, 0, 128), 'teal': (0, 128, 128), 'navy': (0, 0, 128), 'orange': (255, 165, 0), 'gold': (255, 215, 0), 'purple': (128, 0, 128), 'indigo': (75, 0, 130) } class Display: """Represents a single display in a multi-display chain. Contains the coordinates for the slice of the pixel buffer which should be visible on this particular display. """ def __init__(self, enabled, x, y, rotation): """Initialise display. :param enabled: True/False to indicate if this display is enabled :param x: x offset of display portion in buffer :param y: y offset of display portion in buffer :param rotation: rotation of display """ self.enabled = enabled self.update(x, y, rotation) def update(self, x, y, rotation): """Update display position. :param x: x offset of display portion in buffer :param y: y offset of display portion in buffer :param rotation: rotation of display """ self.x = x self.y = y self.rotation = rotation def get_buffer_window(self, source): """Grab the correct portion of the supplied buffer for this display. :param source: source buffer, should be a numpy array """ view = source[self.x:self.x + PANEL_SHAPE[0], self.y:self.y + PANEL_SHAPE[1]] return numpy.rot90(view, self.rotation + 1) _displays = [Display(False, 0, 0, 0) for _ in range(8)] is_setup = False def setup(): """Initialize Unicorn HAT HD.""" global _spi, _buf, is_setup if is_setup: return _spi = spidev.SpiDev() _spi.open(0, 0) _spi.max_speed_hz = 9000000 is_setup = True def enable_addressing(enabled=True): """Enable multi-panel addressing support (for Ubercorn).""" global _addressing_enabled _addressing_enabled = enabled def setup_buffer(width, height): """Set up the internal pixel buffer. :param width: width of buffer, ideally in multiples of 16 :param height: height of buffer, ideally in multiples of 16 """ global _buffer_width, _buffer_height, _buf _buffer_width = width _buffer_height = height _buf = numpy.zeros((_buffer_width, _buffer_height, 3), dtype=int) def enable_display(address, enabled=True): """Enable a single display in the chain. :param address: address of the display from 0 to 7 :param enabled: True/False to indicate display is enabled """ _displays[address].enabled = enabled def setup_display(address, x, y, rotation): """Configure a single display in the chain. :param x: x offset of display portion in buffer :param y: y offset of display portion in buffer :param rotation: rotation of display """ _displays[address].update(x, y, rotation) enable_display(address) def set_brightness(b): """Set the display brightness between 0.0 and 1.0. :param b: Brightness from 0.0 to 1.0 (default 0.5) """ global _brightness _brightness = b def set_rotation(r): """Set the display rotation in degrees. Actual rotation will be snapped to the nearest 90 degrees. """ global _rotation _rotation = int(round(r / 90.0)) def get_rotation(): """Return the display rotation in degrees.""" return _rotation * 90 def set_layout(pixel_map=None): """Do nothing, for library compatibility with Unicorn HAT.""" pass def set_all(r, g, b): """Set all pixels to RGB colour. :param r: Amount of red from 0 to 255 :param g: Amount of green from 0 to 255 :param b: Amount of blue from 0 to 255 """ _buf[:] = r, g, b def set_pixel(x, y, r, g=None, b=None): """Set a single pixel to RGB colour. :param x: Horizontal position from 0 to 15 :param y: Veritcal position from 0 to 15 :param r: Amount of red from 0 to 255 :param g: Amount of green from 0 to 255 :param b: Amount of blue from 0 to 255 """ if type(r) is tuple: r, g, b = r elif type(r) is str: try: r, g, b = COLORS[r.lower()] except KeyError: raise ValueError('Invalid color!') _buf[int(x)][int(y)] = r, g, b def set_pixel_hsv(x, y, h, s=1.0, v=1.0): """Set a single pixel to a colour using HSV. :param x: Horizontal position from 0 to 15 :param y: Veritcal position from 0 to 15 :param h: Hue from 0.0 to 1.0 ( IE: degrees around hue wheel/360.0 ) :param s: Saturation from 0.0 to 1.0 :param v: Value (also known as brightness) from 0.0 to 1.0 """ r, g, b = [int(n * 255) for n in colorsys.hsv_to_rgb(h, s, v)] set_pixel(x, y, r, g, b) def get_pixel(x, y): """Get pixel colour in RGB as a tuple. :param x: Horizontal position from 0 to 15 :param y: Veritcal position from 0 to 15 """ return tuple(_buf[int(x)][int(y)]) def shade_pixels(shader): """Set all pixels to a colour determined by a shader function. :param shader: function that accepts x/y position and returns an r,g,b tuple. """ for x in range(WIDTH): for y in range(HEIGHT): r, g, b = shader(x, y) set_pixel(x, y, r, g, b) def get_pixels(): """Return entire buffer.""" return _buf def get_shape(): """Return the shape (width, height) of the display.""" return _buffer_width, _buffer_height def clear(): """Clear the buffer.""" _buf.fill(0) def off(): """Clear the buffer and immediately update Unicorn HAT HD. Turns off all pixels. """ clear() show() def show(): """Output the contents of the buffer to Unicorn HAT HD.""" setup() if _addressing_enabled: for address in range(8): display = _displays[address] if display.enabled: if _buffer_width == _buffer_height or _rotation in [0, 2]: window = display.get_buffer_window(numpy.rot90(_buf, _rotation)) else: window = display.get_buffer_window(numpy.rot90(_buf, _rotation)) _spi.xfer2([_SOF + 1 + address] + (window.reshape(768) * _brightness).astype(numpy.uint8).tolist()) time.sleep(_DELAY) else: _spi.xfer2([_SOF] + (numpy.rot90(_buf, _rotation).reshape(768) * _brightness).astype(numpy.uint8).tolist()) time.sleep(_DELAY) rotation = set_rotation brightness = set_brightness