#!/usr/bin/env python
"""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)
[docs]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)
[docs] 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
[docs] 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
[docs]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
[docs]def enable_addressing(enabled=True):
"""Enable multi-panel addressing support (for Ubercorn)."""
global _addressing_enabled
_addressing_enabled = enabled
[docs]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)
[docs]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
[docs]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)
[docs]def 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
[docs]def 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))
[docs]def get_rotation():
"""Return the display rotation in degrees."""
return _rotation * 90
[docs]def set_layout(pixel_map=None):
"""Do nothing, for library compatibility with Unicorn HAT."""
pass
[docs]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
[docs]def set_pixel(x, y, r, g, b):
"""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
"""
_buf[int(x)][int(y)] = r, g, b
[docs]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)
[docs]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)])
[docs]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)
[docs]def get_pixels():
"""Return entire buffer."""
return _buf
[docs]def get_shape():
"""Return the shape (width, height) of the display."""
return _buffer_width, _buffer_height
[docs]def clear():
"""Clear the buffer."""
_buf.fill(0)
[docs]def off():
"""Clear the buffer and immediately update Unicorn HAT HD.
Turns off all pixels.
"""
clear()
show()
[docs]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)