"""
.. module:: pixelpi
:platform: Unix
:synopsis: Control of WS281x LEDs with a Raspberry Pi
"""
from gpiozero import OutputDevice
from rpi_ws281x import PixelStrip, ws
[docs]class Strip:
"""
Creates an led Strip with the provided configuration.
:type terminal: int
:param terminal:
Which terminal the terminal is attached to (1-4).
:type size: int or tuple
:param size:
The size of the LED string or matrix. Either the number of LEDs for a string, or (width, height) for a matrix.
:type shape: str, optional
:param shape:
The 'shape' of the LEDs attached:
* :data:`straight` - A LED string (default)
* :data:`reverse` - A LED string which starts at the opposite end.
* :data:`zmatrix` - A matrix where the LEDs in the first row go left to right, the next one
right to left. i.e:
| 1 2 3 4
| 8 7 6 5
| 9 . . .
* :data:`matrix` - a normal matrix where the led order goes left to right. i.e:
| 1 2 3 4
| 5 6 7 8
| 9 . . .
:type ledtype: str
:param ledtype:
One of the supported LED types:
``WS2812`` *(default)*,
``SK6812``,
``SK6812W``,
``SK6812_RGBW``,
``SK6812_RBGW``,
``SK6812_GRBW``,
``SK6812_GBRW``,
``SK6812_BRGW``,
``SK6812_BGRW``,
``WS2811_RGB``,
``WS2811_RBG``,
``WS2811_GRB``,
``WS2811_GBR``,
``WS2811_BRG``,
``WS2811_BGR``
:type brightness: int
:param brightness: The default brightness for all LEDs (0-255).
"""
ledtypeslist = ["WS2812", "SK6812", "SK6812W", "SK6812_RGBW", "SK6812_RBGW", "SK6812_GRBW", "SK6812_GBRW",
"SK6812_BRGW", "SK6812_BGRW", "WS2811_RGB", "WS2811_RBG", "WS2811_GRB", "WS2811_GBR",
"WS2811_BRG", "WS2811_BGR"]
matrixshapelist = ["zmatrix", "matrix"]
stringshapelist = ["straight", "reverse"]
allshapeslist = matrixshapelist + stringshapelist
rotatelist = ["LEFT", "RIGHT", "UP", "DOWN"]
mirrorlist = ["HORIZONTAL", "VERTICAL"]
def __init__(self, terminal, size, shape='straight', ledtype='WS2812', brightness=255):
# ---------------------------------------------
# Which terminal connection is being used (1 to 4)
# ---------------------------------------------
if terminal == 1:
self.__controlpin = 10
self.__channel = 0
self.__onoffpin = 27
elif terminal == 2:
self.__controlpin = 12
self.__channel = 0
self.__onoffpin = 4
elif terminal == 3:
self.__controlpin = 21
self.__channel = 0
self.__onoffpin = 17
elif terminal == 4:
self.__controlpin = 13
self.__channel = 1
self.__onoffpin = 22
else:
raise ValueError('The terminal number must be between 1 and 4.')
self.__stripnum = terminal
# --------------
# The terminal type
# --------------
if ledtype not in self.ledtypeslist:
raise ValueError('This terminal type is not supported.')
self.__striptype = ledtype
supportedstriptypes = {}
for t in ws.__dict__:
if '_STRIP' in t:
k = t.replace('_STRIP', '')
v = getattr(ws, t)
supportedstriptypes[k] = v
self.__internalstriptype = supportedstriptypes[ledtype]
# ---------------
# The terminal shape
# ---------------
if shape not in self.allshapeslist:
raise ValueError('The terminal shape is not supported.')
elif shape in self.matrixshapelist and type(size) is not tuple:
raise ValueError('A matrix shape has been defined, but the size is not a tuple (i.e. (x, y)).')
elif shape not in self.matrixshapelist and type(size) is tuple:
raise ValueError('A non-matrix shape has been defined, but the size is a tuple.')
self.__stripshape = shape
# -----------------------------------------
# The size of the terminal
# striplength is the total number of shift
# -----------------------------------------
if type(size) is tuple:
if len(size) != 2:
raise ValueError('The matrix shape must be defined in width and length size (x, y) only.')
width, height = size
striplength = height * width
self.__width = width
self.__height = height
else:
striplength = size
self.__width = 1
self.__height = striplength
if striplength <= 0:
raise ValueError('The terminal length needs to be 1 or more.')
self.__striplength = striplength
# -----------------------------------
# The default brightness at the start
# -----------------------------------
self.__brightness = self.__checkBrightness(brightness)
# -----------------------------------------------
# A list to hold the led colours and brightness
# -----------------------------------------------
self.__pixels = [[0, 0, 0, self.__brightness]] * self.__striplength
# ----------------------------
# Set up the rpi_ws281x callingclass
# ----------------------------
self.__strip = PixelStrip(self.__striplength, self.__controlpin, 800000, 10, False, self.__brightness,
self.__channel, self.__internalstriptype)
# ---------------------
# Start the terminal logic
# ---------------------
self.__strip.begin()
# -------------------------------------------------------------------
# Set up the pin which defines whether the terminal is written to or not
# -------------------------------------------------------------------
self.__statuspin = OutputDevice(self.__onoffpin, active_high=False, initial_value=False)
self.updateStatus = True
# ---------------
# Clear the terminal
# ---------------
self.clearLEDs()
def __del__(self):
"""Disposes of the rpi_ws281x callingclass"""
del self.__strip
@staticmethod
def __version():
return __version__
@property
def getLength(self):
"""Returns how many LEDs are in the strip/matrix."""
return self.__striplength
@property
def getWidth(self):
"""Returns the width of the matrix (1 if an LED string)."""
return self.__width
@property
def getHeight(self):
"""Returns the height of the matrix (or length of an LED string)."""
return self.__height
@property
def getStripType(self):
"""Returns the set LED type."""
return self.__striptype
@property
def getStripNumber(self):
"""Returns the terminal the LEDs are attached to."""
return self.__stripnum
@property
def updateStatus(self):
"""
Returns or sets whether output is currently enabled for the LEDs.
When set to ``False``, the current LED pattern will remain unchanged even if the pattern is changed and
:class:`showLEDs()` is called.
:getter: Returns the status.
:setter: Sets the status.
:type: bool
"""
return self.__statuspin.value == 1
@updateStatus.setter
def updateStatus(self, status=True):
"""
The setter for updateStatus property.
"""
if status:
self.__statuspin.on()
else:
self.__statuspin.off()
def __checkBrightness(self, brightness):
"""
Checks whether the brightness value given is within range (0-255)
:type brightness: int or None
:param brightness:
A value between 0 (off) and 255 (full brightness)
"""
if brightness is None:
brightness = self.__brightness
else:
if 0 < brightness > 255:
raise ValueError('Brightness must be between 0 and 255')
return brightness
@staticmethod
def __checkRGB(rgb):
"""Checks whether the rgb value passed in is valid."""
if type(rgb) is not tuple:
raise ValueError('The rgb value must be a tuple of the form (r, g, b).')
elif len(rgb) != 3:
raise ValueError('The rgb tuple must have three elements (r, g, b).')
red, green, blue = [int(c) & 0xff for c in rgb]
return red, green, blue
@staticmethod
def __checkPattern(pattern):
"""
Checks whether the pattern passed in is valid
:param pattern: A list of the RGB and brightness values of each led in the terminal
"""
if len(pattern) == 0:
raise ValueError("The pattern must have elements")
if len(pattern[0]) != 4:
raise ValueError("Each index of the pattern must have four elements (red, green, blue, brightness)")
return pattern
def __setBrightness(self, brightness, led=None):
"""
Sets the brightness of one or more LEDs.
:type brightness: int or None
:param brightness: The brightness
:type led: int or None
:param led: If defined, only set that led, otherwise set all
"""
brightness = self.__checkBrightness(brightness)
if led is None:
for led in range(self.__striplength):
self.__pixels[led][3] = brightness
else:
pixelnumber = self.__translate(led)
if 0 <= pixelnumber < self.__striplength:
raise ValueError('The led index is out of range.')
self.__pixels[led][3] = brightness
def __setImage(self, image, position=None):
"""
Plots an RGB image to the matrix or string of LEDs.
:type image: RGB Format image
:param image: An image in RGB format (see PILLOW library)
:type position: int, tuple or None
:param position: The location on the matrix
"""
if image.mode != 'RGB':
raise ValueError("The image must be in RGB format.")
imagewidth, imageheight = image.size
if self.__stripshape in self.matrixshapelist:
if position is None:
position = (0, 0)
elif type(position) is not tuple:
raise ValueError("A matrix shape has been defined, but the size is not a tuple (i.e. (x, y)).")
elif len(position) != 2:
raise ValueError(
'A matrix shape has been defined, but the start position does not have two elements (i.e. (x, y)).')
px, py = position
width = min(imagewidth, self.__width)
height = min(imageheight, self.__height)
else:
if position is None:
position = 0
if type(position) is tuple:
raise ValueError('A non-matrix shape has been defined, but the size is a tuple.')
px, py = position, 0
width = 1
height = min(imageheight, self.__height)
imagecolours = list(image.getdata())
for y in range(height):
for x in range(width):
r, g, b = imagecolours[x + y * 8]
self.setLEDs(led=(px + x, py + y), rgb=(r, g, b))
def __setPattern(self, pattern):
"""
Sets the RGB and brightness of the LEDs using the pattern passed in
:param pattern:
"""
pattern = self.__checkPattern(pattern)
for pixelnumber in range(min(self.__striplength, len(pattern))):
self.__pixels[pixelnumber] = pattern[pixelnumber]
def __translate(self, pixel):
"""Translates co-ordinates for various different shapes"""
realpixel = -1
if self.__stripshape == "straight" or type(pixel) is not tuple:
if 0 <= pixel < self.__striplength:
realpixel = pixel
elif self.__stripshape == "reverse":
if 0 <= pixel < self.__striplength:
realpixel = self.__striplength - pixel
elif self.__stripshape == "zmatrix":
x, y = pixel
if 0 <= x < self.__width and 0 <= y <= self.__height:
if y % 2 == 0:
realpixel = (y * self.__width) + x
else:
realpixel = ((y + 1) * self.__width) - (x + 1)
elif self.__stripshape == "matrix":
x, y = pixel
if 0 <= x < self.__width and 0 <= y <= self.__height:
realpixel = y * self.__width + x
return realpixel
[docs] def getLEDs(self, led=None):
"""
If ``led`` is supplied, returns the RGB and brightness values of a specific LED.
If led is not supplied or set to ``None`` a list of red, green, blue and brightness values for each LED
is returned in a list.
:type led: int, tuple or None
:param led: The led location, either the LED count from the start, or the x,y matrix location,
or if None, a list of all LEDs will be returned
:return: (red, green, blue, brightness) or a list of (red, green, blue, brightness)
"""
if led is None:
return self.__pixels
else:
pixelnumber = self.__translate(led)
if 0 <= pixelnumber < self.__striplength:
r, g, b, brightness = self.__pixels[pixelnumber]
else:
r, g, b, brightness = [0, 0, 0, 0]
return r, g, b, brightness
[docs] def setLEDs(self, led=None, rgb=None, brightness=None, image=None, pattern=None):
"""
Sets the RGB value, and optionally brightness, of one or more LEDs.
If ``led`` is not supplied or set to ``None``, all LEDs will be set to the
defined colour and brightness.
If a matrix is being used, ``led`` can either be the LED count from the first LED,
or the (x, y) location.
If a brightness value is not supplied, or set to ``None``, the current LED brightness
will be kept.
If an image is supplied, then it must be in RGB format (see
`Pillow image file formats <https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html>`_)
If a pattern is supplied, it must be a list consisting of a tuple with four elements,
``(red, green, blue, brightness)``.
:type led: int, tuple or None
:param led: The LED location or None to set all LEDs to the desired colour.
:type rgb: tuple or None
:param rgb: A tuple consisting of 3 elements, (red, green, blue), with each value being between 0 and 255.
:type brightness: int or None
:param brightness: A value between 0 (dim) to 255 (very bright) or None to take the default.
:type image: image or None
:param image: An image callingclass (see PIL or Pillow libraries) containing an RGB formatted image.
:type pattern: list or None
:param pattern: A list of the RGB and brightness values for each LED, in numerical order from the start of the
strip/matrix.
"""
if image is not None:
self.__setImage(image, position=led)
elif pattern is not None:
self.__setPattern(pattern)
elif led is None and rgb is None and brightness is None:
self.clearLEDs()
else:
if rgb is None:
self.__setBrightness(brightness, led)
else:
red, green, blue = self.__checkRGB(rgb)
if led is None:
brightness = self.__checkBrightness(brightness)
self.__pixels = [[red, green, blue, brightness]] * self.__striplength
else:
pixelnumber = self.__translate(led)
if 0 <= pixelnumber < self.__striplength:
if brightness is None:
brightness = self.__pixels[pixelnumber][3]
self.__pixels[pixelnumber] = [red, green, blue, brightness]
[docs] def clearLEDs(self):
"""
Clears the LEDs (sets them to black), leaving the brightness as it is.
"""
for pixel in range(self.__striplength):
self.__pixels[pixel][0:3] = [0, 0, 0]
[docs] def shift(self, direction="UP", shift=1):
"""
Shifts the LEDs on the matrix or string by :data:`shift` LEDs in the direction specified.
:type direction: str
:param direction: The direction the LEDs should shift (LEFT, RIGHT, UP, DOWN)
:type shift: int
:param shift: The number of LEDs the matrix/string should be moved in the specified direction.
"""
direction = direction.upper()
if direction not in self.rotatelist:
raise ValueError('The rotation direction must be one of LEFT, RIGHT, UP or DOWN.')
if self.__stripshape in self.stringshapelist:
if self.__striplength < shift or shift <= 0:
raise ValueError('The shift value must be positive and shorter than the string length.')
if direction == "LEFT" or direction == "UP":
for _ in range(shift):
self.__pixels.insert(0, self.__pixels[self.__striplength - 1])
self.__pixels.pop(self.__striplength)
else:
for _ in range(shift):
self.__pixels.append(self.__pixels[0])
self.__pixels.pop(0)
else:
if direction == "UP" or direction == "DOWN":
if shift >= self.__height:
raise ValueError('The shift must be smaller than the height of the matrix.')
if direction == "DOWN":
shift = -shift
tempstrip = self.__pixels.copy()
for y in range(self.__height):
newy = (y + shift) % self.__height
for x in range(self.__width):
self.__pixels[self.__translate((x, newy))] = tempstrip[self.__translate((x, y))]
else:
if shift >= self.__width:
raise ValueError('The shift must be smaller than the width of the matrix.')
if direction == "LEFT":
shift = -shift
tempstrip = self.__pixels.copy()
for x in range(self.__width):
newx = (x + shift) % self.__width
for y in range(self.__height):
self.__pixels[self.__translate((newx, y))] = tempstrip[self.__translate((x, y))]
[docs] def mirror(self, mirror="VERTICAL"):
"""
Mirrors the matrix LEDs either in the :data:`VERTICAL` or :data:`HORIZONTAL` plane.
Reverses the LED colours on an LED string. The ``mirror`` parameter is not required for strings.
:type mirror: str
:param mirror: :data:`VERTICAL` *(default)* or :data:`HORIZONTAL`
"""
if self.__stripshape in self.matrixshapelist:
mirror = mirror.upper()
if mirror not in self.mirrorlist:
raise ValueError('The mirror value must be either VERTICAL or HORIZONTAL.')
tempstrip = self.__pixels.copy()
if mirror == "VERTICAL":
for y in range(int(self.__height / 2)):
for x in range(self.__width):
self.__pixels[self.__translate((x, y))] = tempstrip[
self.__translate((x, self.__height - 1 - y))]
self.__pixels[self.__translate((x, self.__height - 1 - y))] = tempstrip[
self.__translate((x, y))]
else:
for x in range(int(self.__width / 2)):
for y in range(self.__height):
self.__pixels[self.__translate((x, y))] = tempstrip[
self.__translate((self.__width - 1 - x, y))]
self.__pixels[self.__translate((self.__width - 1 - x, y))] = tempstrip[
self.__translate((x, y))]
else:
self.__pixels.reverse()
[docs] def showLEDs(self):
"""
Once you have set the colours of the string/matrix LEDs, use :class:`showLEDs` to update the LEDs.
"""
for pixel in range(self.__strip.numPixels()):
r, g, b, brightness = self.__pixels[pixel]
self.__strip.setPixelColorRGB(pixel, r, g, b)
self.__strip.show()