Merge pull request #48 from dunkmann00/pixelbuf
Add support for Pixelbuf
This commit is contained in:
commit
f4f66fa039
6 changed files with 98 additions and 192 deletions
|
|
@ -26,7 +26,7 @@ It should be a float. For example, (0xFF,0,0, 1.0) is the brightest red possible
|
|||
|
||||
.. note:: The int hex API represents the brightness of the white pixel when
|
||||
present by setting the RGB channels to identical values. For example, full
|
||||
white is 0xffffff but is actually (0xff, 0xff, 0xff) in the tuple syntax.
|
||||
white is 0xffffff but is actually (0xff, 0xff, 0xff) in the tuple syntax.
|
||||
|
||||
Dependencies
|
||||
=============
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
# Copyright (c) 2016 Damien P. George (original Neopixel object)
|
||||
# Copyright (c) 2017 Ladyada
|
||||
# Copyright (c) 2017 Scott Shawcroft for Adafruit Industries
|
||||
# Copyright (c) 2019 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
|
||||
|
|
@ -23,30 +24,46 @@
|
|||
# THE SOFTWARE.
|
||||
|
||||
"""
|
||||
`adafruit_dotstar` - DotStar strip driver
|
||||
====================================================
|
||||
`adafruit_dotstar` - DotStar strip driver (for CircuitPython 5.0+ with _pixelbuf)
|
||||
=================================================================================
|
||||
|
||||
* Author(s): Damien P. George, Limor Fried & Scott Shawcroft
|
||||
* Author(s): Damien P. George, Limor Fried, Scott Shawcroft & Roy Hooper
|
||||
"""
|
||||
|
||||
# pylint: disable=ungrouped-imports
|
||||
import sys
|
||||
import busio
|
||||
import digitalio
|
||||
|
||||
if sys.implementation.version[0] < 5:
|
||||
import adafruit_pypixelbuf as _pixelbuf
|
||||
else:
|
||||
try:
|
||||
import _pixelbuf
|
||||
except ImportError:
|
||||
import adafruit_pypixelbuf as _pixelbuf
|
||||
|
||||
__version__ = "0.0.0-auto.0"
|
||||
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_DotStar.git"
|
||||
|
||||
START_HEADER_SIZE = 4
|
||||
LED_START = 0b11100000 # Three "1" bits, followed by 5 brightness bits
|
||||
|
||||
# Pixel color order constants
|
||||
RGB = (0, 1, 2)
|
||||
RBG = (0, 2, 1)
|
||||
GRB = (1, 0, 2)
|
||||
GBR = (1, 2, 0)
|
||||
BRG = (2, 0, 1)
|
||||
BGR = (2, 1, 0)
|
||||
RBG = "PRBG"
|
||||
"""Red Blue Green"""
|
||||
RGB = "PRGB"
|
||||
"""Red Green Blue"""
|
||||
GRB = "PGRB"
|
||||
"""Green Red Blue"""
|
||||
GBR = "PGBR"
|
||||
"""Green Blue Red"""
|
||||
BRG = "PBRG"
|
||||
"""Blue Red Green"""
|
||||
BGR = "PBGR"
|
||||
"""Blue Green Red"""
|
||||
|
||||
|
||||
class DotStar:
|
||||
class DotStar(_pixelbuf.PixelBuf):
|
||||
"""
|
||||
A sequence of dotstars.
|
||||
|
||||
|
|
@ -56,16 +73,14 @@ class DotStar:
|
|||
:param float brightness: Brightness of the pixels between 0.0 and 1.0
|
||||
:param bool auto_write: True if the dotstars should immediately change when
|
||||
set. If False, `show` must be called explicitly.
|
||||
:param tuple pixel_order: Set the pixel order on the strip - different
|
||||
strips implement this differently. If you send red, and it looks blue
|
||||
or green on the strip, modify this! It should be one of the values
|
||||
above.
|
||||
:param str pixel_order: Set the pixel order on the strip - different
|
||||
strips implement this differently. If you send red, and it looks blue
|
||||
or green on the strip, modify this! It should be one of the values above.
|
||||
:param int baudrate: Desired clock rate if using hardware SPI (ignored if
|
||||
using 'soft' SPI). This is only a recommendation; the actual clock
|
||||
rate may be slightly different depending on what the system hardware
|
||||
can provide.
|
||||
|
||||
|
||||
Example for Gemma M0:
|
||||
|
||||
.. code-block:: python
|
||||
|
|
@ -79,6 +94,22 @@ class DotStar:
|
|||
with adafruit_dotstar.DotStar(APA102_SCK, APA102_MOSI, 1) as pixels:
|
||||
pixels[0] = RED
|
||||
time.sleep(2)
|
||||
|
||||
.. py:method:: DotStar.show()
|
||||
|
||||
Shows the new colors on the dotstars themselves if they haven't already
|
||||
been autowritten.
|
||||
|
||||
The colors may or may not be showing after this function returns because
|
||||
it may be done asynchronously.
|
||||
|
||||
.. py:method:: DotStar.fill(color)
|
||||
|
||||
Colors all dotstars the given ***color***.
|
||||
|
||||
.. py:attribute:: brightness
|
||||
|
||||
Overall brightness of all dotstars (0 to 1.0)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -105,36 +136,29 @@ class DotStar:
|
|||
self.dpin.direction = digitalio.Direction.OUTPUT
|
||||
self.cpin.direction = digitalio.Direction.OUTPUT
|
||||
self.cpin.value = False
|
||||
self._n = n
|
||||
|
||||
# Supply one extra clock cycle for each two pixels in the strip.
|
||||
self.end_header_size = n // 16
|
||||
trailer_size = n // 16
|
||||
if n % 16 != 0:
|
||||
self.end_header_size += 1
|
||||
self._buf = bytearray(n * 4 + START_HEADER_SIZE + self.end_header_size)
|
||||
self.end_header_index = len(self._buf) - self.end_header_size
|
||||
self.pixel_order = pixel_order
|
||||
# Four empty bytes to start.
|
||||
for i in range(START_HEADER_SIZE):
|
||||
self._buf[i] = 0x00
|
||||
# Mark the beginnings of each pixel.
|
||||
for i in range(START_HEADER_SIZE, self.end_header_index, 4):
|
||||
self._buf[i] = 0xFF
|
||||
# 0xff bytes at the end.
|
||||
for i in range(self.end_header_index, len(self._buf)):
|
||||
self._buf[i] = 0xFF
|
||||
self._brightness = 1.0
|
||||
# Set auto_write to False temporarily so brightness setter does _not_
|
||||
# call show() while in __init__.
|
||||
self.auto_write = False
|
||||
self.brightness = brightness
|
||||
self.auto_write = auto_write
|
||||
trailer_size += 1
|
||||
|
||||
# Four empty bytes for the header.
|
||||
header = bytearray(START_HEADER_SIZE)
|
||||
# 0xff bytes for the trailer.
|
||||
trailer = bytearray(b"\xff") * trailer_size
|
||||
|
||||
super().__init__(
|
||||
n,
|
||||
byteorder=pixel_order,
|
||||
brightness=brightness,
|
||||
auto_write=auto_write,
|
||||
header=header,
|
||||
trailer=trailer,
|
||||
)
|
||||
|
||||
def deinit(self):
|
||||
"""Blank out the DotStars and release the resources."""
|
||||
self.auto_write = False
|
||||
for i in range(START_HEADER_SIZE, self.end_header_index):
|
||||
if i % 4 != 0:
|
||||
self._buf[i] = 0
|
||||
self.fill(0)
|
||||
self.show()
|
||||
if self._spi:
|
||||
self._spi.deinit()
|
||||
|
|
@ -151,136 +175,24 @@ class DotStar:
|
|||
def __repr__(self):
|
||||
return "[" + ", ".join([str(x) for x in self]) + "]"
|
||||
|
||||
def _set_item(self, index, value):
|
||||
"""
|
||||
value can be one of three things:
|
||||
a (r,g,b) list/tuple
|
||||
a (r,g,b, brightness) list/tuple
|
||||
a single, longer int that contains RGB values, like 0xFFFFFF
|
||||
brightness, if specified should be a float 0-1
|
||||
|
||||
Set a pixel value. You can set per-pixel brightness here, if it's not passed it
|
||||
will use the max value for pixel brightness value, which is a good default.
|
||||
|
||||
Important notes about the per-pixel brightness - it's accomplished by
|
||||
PWMing the entire output of the LED, and that PWM is at a much
|
||||
slower clock than the rest of the LEDs. This can cause problems in
|
||||
Persistence of Vision Applications
|
||||
"""
|
||||
|
||||
offset = index * 4 + START_HEADER_SIZE
|
||||
rgb = value
|
||||
if isinstance(value, int):
|
||||
rgb = (value >> 16, (value >> 8) & 0xFF, value & 0xFF)
|
||||
|
||||
if len(rgb) == 4:
|
||||
brightness = value[3]
|
||||
# Ignore value[3] below.
|
||||
else:
|
||||
brightness = 1
|
||||
|
||||
# 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
|
||||
# same as math.ceil(brightness * 31) & 0b00011111
|
||||
# Idea from https://www.codeproject.com/Tips/700780/Fast-floor-ceiling-functions
|
||||
brightness_byte = 32 - int(32 - brightness * 31) & 0b00011111
|
||||
self._buf[offset] = brightness_byte | LED_START
|
||||
self._buf[offset + 1] = rgb[self.pixel_order[0]]
|
||||
self._buf[offset + 2] = rgb[self.pixel_order[1]]
|
||||
self._buf[offset + 3] = rgb[self.pixel_order[2]]
|
||||
|
||||
def __setitem__(self, index, val):
|
||||
if isinstance(index, slice):
|
||||
start, stop, step = index.indices(self._n)
|
||||
length = stop - start
|
||||
if step != 0:
|
||||
# same as math.ceil(length / step)
|
||||
# Idea from https://fizzbuzzer.com/implement-a-ceil-function/
|
||||
length = (length + step - 1) // step
|
||||
if len(val) != length:
|
||||
raise ValueError("Slice and input sequence size do not match.")
|
||||
for val_i, in_i in enumerate(range(start, stop, step)):
|
||||
self._set_item(in_i, val[val_i])
|
||||
else:
|
||||
self._set_item(index, val)
|
||||
|
||||
if self.auto_write:
|
||||
self.show()
|
||||
|
||||
def __getitem__(self, index):
|
||||
if isinstance(index, slice):
|
||||
out = []
|
||||
for in_i in range(*index.indices(self._n)):
|
||||
out.append(
|
||||
tuple(
|
||||
self._buf[in_i * 4 + (3 - i) + START_HEADER_SIZE]
|
||||
for i in range(3)
|
||||
)
|
||||
)
|
||||
return out
|
||||
if index < 0:
|
||||
index += len(self)
|
||||
if index >= self._n or index < 0:
|
||||
raise IndexError
|
||||
offset = index * 4
|
||||
return tuple(self._buf[offset + (3 - i) + START_HEADER_SIZE] for i in range(3))
|
||||
|
||||
def __len__(self):
|
||||
return self._n
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Overall brightness of the pixel"""
|
||||
return self._brightness
|
||||
def n(self):
|
||||
"""
|
||||
The number of dotstars in the chain (read-only)
|
||||
"""
|
||||
return len(self)
|
||||
|
||||
@brightness.setter
|
||||
def brightness(self, brightness):
|
||||
self._brightness = min(max(brightness, 0.0), 1.0)
|
||||
if self.auto_write:
|
||||
self.show()
|
||||
def _transmit(self, buffer):
|
||||
if self._spi:
|
||||
self._spi.write(buffer)
|
||||
else:
|
||||
self._ds_writebytes(buffer)
|
||||
|
||||
def fill(self, color):
|
||||
"""Colors all pixels the given ***color***."""
|
||||
auto_write = self.auto_write
|
||||
self.auto_write = False
|
||||
for i in range(self._n):
|
||||
self[i] = color
|
||||
if auto_write:
|
||||
self.show()
|
||||
self.auto_write = auto_write
|
||||
|
||||
def _ds_writebytes(self, buf):
|
||||
for b in buf:
|
||||
def _ds_writebytes(self, buffer):
|
||||
for b in buffer:
|
||||
for _ in range(8):
|
||||
self.dpin.value = b & 0x80
|
||||
self.cpin.value = True
|
||||
self.cpin.value = False
|
||||
b = b << 1
|
||||
|
||||
def show(self):
|
||||
"""Shows the new colors on the pixels themselves if they haven't already
|
||||
been autowritten.
|
||||
|
||||
The colors may or may not be showing after this function returns because
|
||||
it may be done asynchronously."""
|
||||
# Create a second output buffer if we need to compute brightness
|
||||
buf = self._buf
|
||||
if self.brightness < 1.0:
|
||||
buf = bytearray(self._buf)
|
||||
# Four empty bytes to start.
|
||||
for i in range(START_HEADER_SIZE):
|
||||
buf[i] = 0x00
|
||||
for i in range(START_HEADER_SIZE, self.end_header_index):
|
||||
buf[i] = (
|
||||
self._buf[i] if i % 4 == 0 else int(self._buf[i] * self._brightness)
|
||||
)
|
||||
# Four 0xff bytes at the end.
|
||||
for i in range(self.end_header_index, len(buf)):
|
||||
buf[i] = 0xFF
|
||||
|
||||
if self._spi:
|
||||
self._spi.write(buf)
|
||||
else:
|
||||
self._ds_writebytes(buf)
|
||||
self.cpin.value = False
|
||||
self.cpin.value = False
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
# than through function calls or setters/getters...this is poor form as it
|
||||
# could break easily with future library changes, but is the only way right
|
||||
# now to do the POV as quickly as possible.
|
||||
# May require installing separate libraries.
|
||||
|
||||
import board
|
||||
from PIL import Image
|
||||
|
|
@ -25,7 +26,7 @@ DOTS = dotstar.DotStar(
|
|||
board.MOSI,
|
||||
NUMPIXELS,
|
||||
auto_write=False,
|
||||
brightness=0.25,
|
||||
brightness=1.0,
|
||||
pixel_order=ORDER,
|
||||
)
|
||||
|
||||
|
|
@ -42,41 +43,29 @@ if HEIGHT > NUMPIXELS:
|
|||
|
||||
# Calculate gamma correction table, makes mid-range colors look 'right':
|
||||
GAMMA = bytearray(256)
|
||||
brightness = 0.25
|
||||
for i in range(256):
|
||||
# Notice we access DOTS.brightness directly here...the gamma table will
|
||||
# handle any brightness-scaling, so we can set the object brightness back
|
||||
# to max and it won't need to perform brightness scaling on every write.
|
||||
GAMMA[i] = int(pow(float(i) / 255.0, 2.7) * DOTS.brightness * 255.0 + 0.5)
|
||||
DOTS.brightness = 1.0
|
||||
GAMMA[i] = int(pow(float(i) / 255.0, 2.7) * brightness * 255.0 + 0.5)
|
||||
|
||||
# Allocate list of bytearrays, one for each column of image.
|
||||
# Each pixel REQUIRES 4 bytes (0xFF, B, G, R).
|
||||
# Allocate list of lists, one for each column of image.
|
||||
print("Allocating...")
|
||||
COLUMN = [0 for x in range(WIDTH)]
|
||||
for x in range(WIDTH):
|
||||
COLUMN[x] = bytearray(HEIGHT * 4)
|
||||
COLUMN[x] = [[0, 0, 0, 0] for _ in range(HEIGHT)]
|
||||
|
||||
# Convert entire RGB image into column-wise bytearray list.
|
||||
# The dotstar_image_paint.py example uses the library's 'setter' operation
|
||||
# for each pixel to do any R/G/B reordering. Because we're preparing data
|
||||
# directly for the strip, there's a reference to 'ORDER' here to rearrange
|
||||
# the color bytes as needed.
|
||||
# Convert entire RGB image into columnxrow 2D list.
|
||||
print("Converting...")
|
||||
for x in range(WIDTH): # For each column of image
|
||||
for y in range(HEIGHT): # For each pixel in column
|
||||
value = PIXELS[x, y] # Read RGB pixel in image
|
||||
y4 = y * 4 # Position in raw buffer
|
||||
COLUMN[x][y4] = 0xFF # Pixel start marker
|
||||
y4 += 1 # Pixel color data start
|
||||
COLUMN[x][y4 + ORDER[0]] = GAMMA[value[0]] # Gamma-corrected R
|
||||
COLUMN[x][y4 + ORDER[1]] = GAMMA[value[1]] # Gamma-corrected G
|
||||
COLUMN[x][y4 + ORDER[2]] = GAMMA[value[2]] # Gamma-corrected B
|
||||
COLUMN[x][y][0] = GAMMA[value[0]] # Gamma-corrected R
|
||||
COLUMN[x][y][1] = GAMMA[value[1]] # Gamma-corrected G
|
||||
COLUMN[x][y][2] = GAMMA[value[2]] # Gamma-corrected B
|
||||
COLUMN[x][y][3] = 1.0 # Brightness
|
||||
|
||||
print("Displaying...")
|
||||
while True: # Loop forever
|
||||
|
||||
# pylint: disable=protected-access
|
||||
# (Really shouldn't access _buf directly, but needed for fastest POV)
|
||||
for x in range(WIDTH): # For each column of image...
|
||||
DOTS._buf[4 : 4 + HEIGHT * 4] = COLUMN[x] # Copy column to DotStar buffer
|
||||
DOTS[0 : DOTS.n] = COLUMN[x] # Copy column to DotStar buffer
|
||||
DOTS.show() # Send data to strip
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ dots = dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1, brightness=0.2)
|
|||
|
||||
|
||||
# HELPERS
|
||||
# a random color 0 -> 224
|
||||
# a random color 0 -> 192
|
||||
def random_color():
|
||||
return random.randrange(0, 7) * 32
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
Adafruit-Blinka
|
||||
adafruit-circuitpython-busdevice
|
||||
adafruit-circuitpython-pypixelbuf>=2.0.0
|
||||
|
|
|
|||
6
setup.py
6
setup.py
|
|
@ -30,7 +30,11 @@ setup(
|
|||
# Author details
|
||||
author="Adafruit Industries",
|
||||
author_email="circuitpython@adafruit.com",
|
||||
install_requires=["Adafruit-Blinka", "adafruit-circuitpython-busdevice"],
|
||||
install_requires=[
|
||||
"Adafruit-Blinka",
|
||||
"adafruit-circuitpython-busdevice",
|
||||
"adafruit-circuitpython-pypixelbuf>=2.0.0",
|
||||
],
|
||||
# Choose your license
|
||||
license="MIT",
|
||||
# See https://pypi.python.org/pypi?%3Aaction=list_classifiers
|
||||
|
|
|
|||
Loading…
Reference in a new issue