375 lines
12 KiB
Python
375 lines
12 KiB
Python
|
# The MIT License (MIT)
|
||
|
#
|
||
|
# Based on the Adafruit NeoPixel and Adafruit Dotstar CircuitPython drivers.
|
||
|
# Copyright (c) 2019-2020 Roy Hooper
|
||
|
#
|
||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
|
# of this software and associated documentation files (the "Software"), to deal
|
||
|
# in the Software without restriction, including without limitation the rights
|
||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
|
# copies of the Software, and to permit persons to whom the Software is
|
||
|
# furnished to do so, subject to the following conditions:
|
||
|
#
|
||
|
# The above copyright notice and this permission notice shall be included in
|
||
|
# all copies or substantial portions of the Software.
|
||
|
#
|
||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||
|
# THE SOFTWARE.
|
||
|
|
||
|
"""
|
||
|
`adafruit_pypixelbuf` - A pure python implementation of _pixelbuf
|
||
|
=================================================================
|
||
|
This class is used when _pixelbuf is not available in CircuitPython. It is based on the work
|
||
|
in neopixel.py and adafruit_dotstar.py.
|
||
|
|
||
|
* Author(s): Damien P. George & Limor Fried & Scott Shawcroft & Roy Hooper
|
||
|
"""
|
||
|
|
||
|
DOTSTAR_LED_START_FULL_BRIGHT = 0xFF
|
||
|
DOTSTAR_LED_START = 0b11100000 # Three "1" bits, followed by 5 brightness bits
|
||
|
DOTSTAR_LED_BRIGHTNESS = 0b00011111
|
||
|
|
||
|
|
||
|
class PixelBuf: # pylint: disable=too-many-instance-attributes
|
||
|
"""
|
||
|
A sequence of RGB/RGBW pixels.
|
||
|
|
||
|
This is the pure python implementation of CircuitPython's _pixelbuf.
|
||
|
|
||
|
:param ~int n: Number of pixels
|
||
|
:param ~str byteorder: Byte order string constant (also sets bpp)
|
||
|
:param ~float brightness: Brightness (0 to 1.0, default 1.0)
|
||
|
:param ~bool auto_write: Whether to automatically write pixels (Default False)
|
||
|
:param bytes header: Sequence of bytes to always send before pixel values.
|
||
|
:param bytes trailer: Sequence of bytes to always send after pixel values.
|
||
|
"""
|
||
|
|
||
|
def __init__( # pylint: disable=too-many-locals,too-many-arguments
|
||
|
self,
|
||
|
n,
|
||
|
byteorder="BGR",
|
||
|
brightness=1.0,
|
||
|
auto_write=False,
|
||
|
header=None,
|
||
|
trailer=None,
|
||
|
):
|
||
|
|
||
|
bpp, byteorder_tuple, has_white, dotstar_mode = self.parse_byteorder(byteorder)
|
||
|
|
||
|
self.auto_write = False
|
||
|
|
||
|
effective_bpp = 4 if dotstar_mode else bpp
|
||
|
_bytes = effective_bpp * n
|
||
|
buf = bytearray(_bytes)
|
||
|
offset = 0
|
||
|
|
||
|
if header is not None:
|
||
|
if not isinstance(header, bytearray):
|
||
|
raise TypeError("header must be a bytearray")
|
||
|
buf = header + buf
|
||
|
offset = len(header)
|
||
|
|
||
|
if trailer is not None:
|
||
|
if not isinstance(trailer, bytearray):
|
||
|
raise TypeError("trailer must be a bytearray")
|
||
|
buf += trailer
|
||
|
|
||
|
self._pixels = n
|
||
|
self._bytes = _bytes
|
||
|
self._byteorder = byteorder_tuple
|
||
|
self._byteorder_string = byteorder
|
||
|
self._has_white = has_white
|
||
|
self._bpp = bpp
|
||
|
self._pre_brightness_buffer = None
|
||
|
self._post_brightness_buffer = buf
|
||
|
self._offset = offset
|
||
|
self._dotstar_mode = dotstar_mode
|
||
|
self._pixel_step = effective_bpp
|
||
|
|
||
|
if dotstar_mode:
|
||
|
self._byteorder_tuple = (
|
||
|
byteorder_tuple[0] + 1,
|
||
|
byteorder_tuple[1] + 1,
|
||
|
byteorder_tuple[2] + 1,
|
||
|
0,
|
||
|
)
|
||
|
# Initialize the buffer with the dotstar start bytes.
|
||
|
for i in range(self._offset, self._bytes + self._offset, 4):
|
||
|
self._post_brightness_buffer[i] = DOTSTAR_LED_START_FULL_BRIGHT
|
||
|
|
||
|
self._brightness = 1.0
|
||
|
self.brightness = brightness
|
||
|
|
||
|
self.auto_write = auto_write
|
||
|
|
||
|
@staticmethod
|
||
|
def parse_byteorder(byteorder):
|
||
|
"""
|
||
|
Parse a Byteorder string for validity and determine bpp, byte order, and
|
||
|
dostar brightness bits.
|
||
|
|
||
|
Byteorder strings may contain the following characters:
|
||
|
R - Red
|
||
|
G - Green
|
||
|
B - Blue
|
||
|
W - White
|
||
|
P - PWM (PWM Duty cycle for pixel - dotstars 0 - 1.0)
|
||
|
|
||
|
:param: ~str bpp: bpp string.
|
||
|
:return: ~tuple: bpp, byteorder, has_white, dotstar_mode
|
||
|
"""
|
||
|
bpp = len(byteorder)
|
||
|
dotstar_mode = False
|
||
|
has_white = False
|
||
|
|
||
|
if byteorder.strip("RGBWP") != "":
|
||
|
raise ValueError("Invalid Byteorder string")
|
||
|
|
||
|
try:
|
||
|
r = byteorder.index("R")
|
||
|
g = byteorder.index("G")
|
||
|
b = byteorder.index("B")
|
||
|
except ValueError:
|
||
|
raise ValueError("Invalid Byteorder string")
|
||
|
if "W" in byteorder:
|
||
|
w = byteorder.index("W")
|
||
|
byteorder = (r, g, b, w)
|
||
|
has_white = True
|
||
|
elif "P" in byteorder:
|
||
|
lum = byteorder.index("P")
|
||
|
byteorder = (r, g, b, lum)
|
||
|
dotstar_mode = True
|
||
|
else:
|
||
|
byteorder = (r, g, b)
|
||
|
|
||
|
return bpp, byteorder, has_white, dotstar_mode
|
||
|
|
||
|
@property
|
||
|
def bpp(self):
|
||
|
"""
|
||
|
The number of bytes per pixel in the buffer (read-only).
|
||
|
"""
|
||
|
return self._bpp
|
||
|
|
||
|
@property
|
||
|
def brightness(self):
|
||
|
"""
|
||
|
Float value between 0 and 1. Output brightness.
|
||
|
|
||
|
When brightness is less than 1.0, a second buffer will be used to store the color values
|
||
|
before they are adjusted for brightness.
|
||
|
"""
|
||
|
return self._brightness
|
||
|
|
||
|
@brightness.setter
|
||
|
def brightness(self, value):
|
||
|
value = min(max(value, 0.0), 1.0)
|
||
|
change = value - self._brightness
|
||
|
if -0.001 < change < 0.001:
|
||
|
return
|
||
|
|
||
|
self._brightness = value
|
||
|
|
||
|
if self._pre_brightness_buffer is None:
|
||
|
self._pre_brightness_buffer = bytearray(self._post_brightness_buffer)
|
||
|
|
||
|
# Adjust brightness of existing pixels
|
||
|
offset_check = self._offset % self._pixel_step
|
||
|
for i in range(self._offset, self._bytes + self._offset):
|
||
|
# Don't adjust per-pixel luminance bytes in dotstar mode
|
||
|
if self._dotstar_mode and (i % 4 != offset_check):
|
||
|
continue
|
||
|
self._post_brightness_buffer[i] = int(
|
||
|
self._pre_brightness_buffer[i] * self._brightness
|
||
|
)
|
||
|
|
||
|
if self.auto_write:
|
||
|
self.show()
|
||
|
|
||
|
@property
|
||
|
def byteorder(self):
|
||
|
"""
|
||
|
ByteOrder string for the buffer (read-only)
|
||
|
"""
|
||
|
return self._byteorder_string
|
||
|
|
||
|
def __len__(self):
|
||
|
"""
|
||
|
Number of pixels.
|
||
|
"""
|
||
|
return self._pixels
|
||
|
|
||
|
def show(self):
|
||
|
"""
|
||
|
Call the associated write function to display the pixels
|
||
|
"""
|
||
|
return self._transmit(self._post_brightness_buffer)
|
||
|
|
||
|
def fill(self, color):
|
||
|
"""
|
||
|
Fills the given pixelbuf with the given color.
|
||
|
:param pixelbuf: A pixel object.
|
||
|
:param color: Color to set.
|
||
|
"""
|
||
|
r, g, b, w = self._parse_color(color)
|
||
|
for i in range(self._pixels):
|
||
|
self._set_item(i, r, g, b, w)
|
||
|
if self.auto_write:
|
||
|
self.show()
|
||
|
|
||
|
def _parse_color(self, value):
|
||
|
r = 0
|
||
|
g = 0
|
||
|
b = 0
|
||
|
w = 0
|
||
|
if isinstance(value, int):
|
||
|
r = value >> 16
|
||
|
g = (value >> 8) & 0xFF
|
||
|
b = value & 0xFF
|
||
|
w = 0
|
||
|
|
||
|
if self._dotstar_mode:
|
||
|
w = 1.0
|
||
|
else:
|
||
|
if len(value) < 3 or len(value) > 4:
|
||
|
raise ValueError(
|
||
|
"Expected tuple of length {}, got {}".format(self._bpp, len(value))
|
||
|
)
|
||
|
if len(value) == self._bpp:
|
||
|
if self._bpp == 3:
|
||
|
r, g, b = value
|
||
|
else:
|
||
|
r, g, b, w = value
|
||
|
elif len(value) == 3:
|
||
|
r, g, b = value
|
||
|
if self._dotstar_mode:
|
||
|
w = 1.0
|
||
|
|
||
|
if self._bpp == 4:
|
||
|
if self._dotstar_mode:
|
||
|
# LED startframe is three "1" bits, followed by 5 brightness bits
|
||
|
# then 8 bits for each of R, G, and B. The order of those 3 are configurable and
|
||
|
# vary based on hardware
|
||
|
w = (int(w * 31) & 0b00011111) | DOTSTAR_LED_START
|
||
|
elif (
|
||
|
self._has_white
|
||
|
and (isinstance(value, int) or len(value) == 3)
|
||
|
and r == g
|
||
|
and g == b
|
||
|
):
|
||
|
# If all components are the same and we have a white pixel then use it
|
||
|
# instead of the individual components when all 4 values aren't explicitly given.
|
||
|
w = r
|
||
|
r = 0
|
||
|
g = 0
|
||
|
b = 0
|
||
|
|
||
|
return (r, g, b, w)
|
||
|
|
||
|
def _set_item(
|
||
|
self, index, r, g, b, w
|
||
|
): # pylint: disable=too-many-locals,too-many-branches,too-many-arguments
|
||
|
if index < 0:
|
||
|
index += len(self)
|
||
|
if index >= self._pixels or index < 0:
|
||
|
raise IndexError
|
||
|
offset = self._offset + (index * self._bpp)
|
||
|
|
||
|
if self._pre_brightness_buffer is not None:
|
||
|
if self._bpp == 4:
|
||
|
self._pre_brightness_buffer[offset + self._byteorder[3]] = w
|
||
|
self._pre_brightness_buffer[offset + self._byteorder[0]] = r
|
||
|
self._pre_brightness_buffer[offset + self._byteorder[1]] = g
|
||
|
self._pre_brightness_buffer[offset + self._byteorder[2]] = b
|
||
|
|
||
|
if self._bpp == 4:
|
||
|
# Only apply brightness if w is actually white (aka not DotStar.)
|
||
|
if not self._dotstar_mode:
|
||
|
w = int(w * self._brightness)
|
||
|
self._post_brightness_buffer[offset + self._byteorder[3]] = w
|
||
|
|
||
|
self._post_brightness_buffer[offset + self._byteorder[0]] = int(
|
||
|
r * self._brightness
|
||
|
)
|
||
|
self._post_brightness_buffer[offset + self._byteorder[1]] = int(
|
||
|
g * self._brightness
|
||
|
)
|
||
|
self._post_brightness_buffer[offset + self._byteorder[2]] = int(
|
||
|
b * self._brightness
|
||
|
)
|
||
|
|
||
|
def __setitem__(self, index, val):
|
||
|
if isinstance(index, slice):
|
||
|
start, stop, step = index.indices(self._pixels)
|
||
|
for val_i, in_i in enumerate(range(start, stop, step)):
|
||
|
r, g, b, w = self._parse_color(val[val_i])
|
||
|
self._set_item(in_i, r, g, b, w)
|
||
|
else:
|
||
|
r, g, b, w = self._parse_color(val)
|
||
|
self._set_item(index, r, g, b, w)
|
||
|
|
||
|
if self.auto_write:
|
||
|
self.show()
|
||
|
|
||
|
def _getitem(self, index):
|
||
|
start = self._offset + (index * self._bpp)
|
||
|
buffer = (
|
||
|
self._pre_brightness_buffer
|
||
|
if self._pre_brightness_buffer is not None
|
||
|
else self._post_brightness_buffer
|
||
|
)
|
||
|
value = [
|
||
|
buffer[start + self._byteorder[0]],
|
||
|
buffer[start + self._byteorder[1]],
|
||
|
buffer[start + self._byteorder[2]],
|
||
|
]
|
||
|
if self._has_white:
|
||
|
value.append(buffer[start + self._byteorder[3]])
|
||
|
elif self._dotstar_mode:
|
||
|
value.append(
|
||
|
(buffer[start + self._byteorder[3]] & DOTSTAR_LED_BRIGHTNESS) / 31.0
|
||
|
)
|
||
|
return value
|
||
|
|
||
|
def __getitem__(self, index):
|
||
|
if isinstance(index, slice):
|
||
|
out = []
|
||
|
for in_i in range(
|
||
|
*index.indices(len(self._post_brightness_buffer) // self._bpp)
|
||
|
):
|
||
|
out.append(self._getitem(in_i))
|
||
|
return out
|
||
|
if index < 0:
|
||
|
index += len(self)
|
||
|
if index >= self._pixels or index < 0:
|
||
|
raise IndexError
|
||
|
return self._getitem(index)
|
||
|
|
||
|
def _transmit(self, buffer):
|
||
|
raise NotImplementedError("Must be subclassed")
|
||
|
|
||
|
|
||
|
def wheel(pos):
|
||
|
"""
|
||
|
Helper to create a colorwheel.
|
||
|
|
||
|
:param pos: int 0-255 of color value to return
|
||
|
:return: tuple of RGB values
|
||
|
"""
|
||
|
# Input a value 0 to 255 to get a color value.
|
||
|
# The colours are a transition r - g - b - back to r.
|
||
|
if pos < 0 or pos > 255:
|
||
|
return 0, 0, 0
|
||
|
if pos < 85:
|
||
|
return 255 - pos * 3, pos * 3, 0
|
||
|
if pos < 170:
|
||
|
pos -= 85
|
||
|
return 0, 255 - pos * 3, pos * 3
|
||
|
pos -= 170
|
||
|
return pos * 3, 0, 255 - pos * 3
|