diff --git a/README.rst b/README.rst index 1c4f99c..cd83811 100644 --- a/README.rst +++ b/README.rst @@ -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 ============= diff --git a/adafruit_dotstar.py b/adafruit_dotstar.py index 61b2a5c..65fbbe8 100755 --- a/adafruit_dotstar.py +++ b/adafruit_dotstar.py @@ -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) - - -class DotStar: +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(_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 @@ def __init__( 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 @@ def __exit__(self, exception_type, exception_value, traceback): def __repr__(self): return "[" + ", ".join([str(x) for x in self]) + "]" - def _set_item(self, index, value): + @property + def n(self): """ - 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 + The number of dotstars in the chain (read-only) """ + return len(self) - 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]) + def _transmit(self, buffer): + if self._spi: + self._spi.write(buffer) 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 + self._ds_writebytes(buffer) - @property - def brightness(self): - """Overall brightness of the pixel""" - return self._brightness - - @brightness.setter - def brightness(self, brightness): - self._brightness = min(max(brightness, 0.0), 1.0) - if self.auto_write: - self.show() - - 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 diff --git a/examples/dotstar_image_pov.py b/examples/dotstar_image_pov.py index cd70a6c..8ea7edf 100644 --- a/examples/dotstar_image_pov.py +++ b/examples/dotstar_image_pov.py @@ -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 @@ board.MOSI, NUMPIXELS, auto_write=False, - brightness=0.25, + brightness=1.0, pixel_order=ORDER, ) @@ -42,41 +43,29 @@ # 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 diff --git a/examples/dotstar_simpletest.py b/examples/dotstar_simpletest.py index 4eb2df0..88203b8 100644 --- a/examples/dotstar_simpletest.py +++ b/examples/dotstar_simpletest.py @@ -14,7 +14,7 @@ # HELPERS -# a random color 0 -> 224 +# a random color 0 -> 192 def random_color(): return random.randrange(0, 7) * 32 diff --git a/requirements.txt b/requirements.txt index 3031961..428f175 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ Adafruit-Blinka adafruit-circuitpython-busdevice +adafruit-circuitpython-pypixelbuf>=2.0.0 diff --git a/setup.py b/setup.py index d28e64c..9c3e867 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,11 @@ # 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