diff --git a/adafruit_ws2801.py b/adafruit_ws2801.py index 021cdf9..87d5d24 100644 --- a/adafruit_ws2801.py +++ b/adafruit_ws2801.py @@ -2,6 +2,7 @@ # SPDX-FileCopyrightText: 2017 Ladyada for Adafruit Industries # SPDX-FileCopyrightText: 2017 Scott Shawcroft for Adafruit Industries # SPDX-FileCopyrightText: 2018 Kevin J. Walters +# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries # # SPDX-License-Identifier: MIT @@ -9,15 +10,17 @@ `adafruit_ws2801` - WS2801 LED pixel string driver ==================================================== -* Author(s): Damien P. George, Limor Fried & Scott Shawcroft, Kevin J Walters +* Author(s): Damien P. George, Limor Fried & Scott Shawcroft, Kevin J Walters, Tim Cocks """ -import math +import adafruit_pixelbuf import busio import digitalio try: - from typing import Any, Union, Tuple, List + from typing import Type, Optional + from circuitpython_typing import ReadableBuffer + from types import TracebackType from microcontroller import Pin except ImportError: pass @@ -27,8 +30,22 @@ # based on https://github.com/adafruit/Adafruit_CircuitPython_DotStar - -class WS2801: +# Pixel color order constants +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 WS2801(adafruit_pixelbuf.PixelBuf): """ A sequence of WS2801 controlled LEDs. @@ -38,7 +55,9 @@ class WS2801: :param float brightness: The brightness between 0.0 and (default) 1.0. :param bool auto_write: True if the dotstars should immediately change when set. If False, `show` must be called explicitly. - + :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. Example for Gemma M0: @@ -53,16 +72,34 @@ class WS2801: with adafruit_ws2801.WS2801(board.D2, board.D0, 25, brightness=1.0) as pixels: pixels[0] = darkred time.sleep(2) + + .. py:method:: show() + + Shows the new colors on the ws2801 LEDs 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:: fill(color) + + Colors all ws2801 LEDs the given ***color***. + + .. py:attribute:: brightness + + Overall brightness of all ws2801 LEDs (0 to 1.0) + """ - def __init__( + def __init__( # pylint: disable=too-many-arguments self, clock: Pin, data: Pin, n: int, *, brightness: float = 1.0, - auto_write: bool = True + auto_write: bool = True, + pixel_order: str = "RGB", ) -> None: self._spi = None try: @@ -76,21 +113,31 @@ def __init__( self.dpin.direction = digitalio.Direction.OUTPUT self.cpin.direction = digitalio.Direction.OUTPUT self.cpin.value = False - self._n = n - self._buf = bytearray(n * 3) - self._brightness = 1.0 # keeps pylint happy - # 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 - # TODO - review/consider adding GRB support like that in c++ version + + # Supply one extra clock cycle for each two pixels in the strip. + trailer_size = n // 16 + if n % 16 != 0: + trailer_size += 1 + + # Empty header. + header = bytearray(0) + # Zero bits, not ones, for the trailer, to avoid lighting up + # downstream pixels, if there are more physical pixels than + # the length of this object. + trailer = bytearray(trailer_size) + + super().__init__( + n, + byteorder=pixel_order, + brightness=brightness, + auto_write=auto_write, + header=header, + trailer=trailer, + ) def deinit(self) -> None: - """Blank out the DotStars and release the resources.""" - self.auto_write = False - black = (0, 0, 0) - self.fill(black) + """Blank out the ws2801 LEDs and release the resources.""" + self.fill(0) self.show() if self._spi: self._spi.deinit() @@ -102,81 +149,16 @@ def __enter__(self) -> "WS2801": return self def __exit__( - self, exception_type: Any, exception_value: Any, traceback: Any + self, + exception_type: Optional[Type[type]], + exception_value: Optional[BaseException], + traceback: Optional[TracebackType], ) -> None: self.deinit() def __repr__(self): return "[" + ", ".join([str(x) for x in self]) + "]" - def _set_item(self, index: int, value: Union[Tuple[int, ...], int]): - offset = index * 3 - if isinstance(value, int): - r = value >> 16 - g = (value >> 8) & 0xFF - b = value & 0xFF - else: - r, g, b = value - # red/green/blue order for WS2801 - self._buf[offset] = r - self._buf[offset + 1] = g - self._buf[offset + 2] = b - - def __setitem__(self, index: int, val: Union[Tuple[int, ...], int]): - if isinstance(index, slice): - start, stop, step = index.indices(self._n) - length = stop - start - if step != 0: - length = math.ceil(length / 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: Union[slice, int] - ) -> Union[Tuple[int, ...], List[Tuple[int, ...]]]: - if isinstance(index, slice): - out = [] - for in_i in range(*index.indices(self._n)): - out.append(tuple(self._buf[in_i * 3 + i] for i in range(3))) - return out - if index < 0: - index += len(self) - if index >= self._n or index < 0: - raise IndexError - offset = index * 3 - return tuple(self._buf[offset + i] for i in range(3)) - - def __len__(self) -> int: - return self._n - - @property - def brightness(self) -> float: - """Overall brightness of the pixel""" - return self._brightness - - @brightness.setter - def brightness(self, brightness: float) -> None: - self._brightness = min(max(brightness, 0.0), 1.0) - if self.auto_write: - self.show() - - def fill(self, color: Union[Tuple[int, ...], int]) -> None: - """Colors all pixels the given ***color***.""" - auto_write = self.auto_write - self.auto_write = False - for i, _ in enumerate(self): - self[i] = color - if auto_write: - self.show() - self.auto_write = auto_write - def _ds_writebytes(self, buf: bytearray) -> None: for b in buf: for _ in range(8): @@ -185,21 +167,8 @@ def _ds_writebytes(self, buf: bytearray) -> None: self.cpin.value = False b = b << 1 - def show(self) -> None: - """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(len(self._buf)) - for i, val in enumerate(self._buf): - buf[i] = int(val * self._brightness) - + def _transmit(self, buffer: ReadableBuffer) -> None: if self._spi: - self._spi.write(buf) + self._spi.write(buffer) else: - self._ds_writebytes(buf) - self.cpin.value = False + self._ds_writebytes(buffer) diff --git a/docs/api.rst b/docs/api.rst index 7cb052c..7bca07a 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -4,5 +4,8 @@ .. If your library file(s) are nested in a directory (e.g. /adafruit_foo/foo.py) .. use this format as the module name: "adafruit_foo.foo" +API Reference +============= + .. automodule:: adafruit_ws2801 :members: diff --git a/requirements.txt b/requirements.txt index a45c547..03bd9cb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ Adafruit-Blinka adafruit-circuitpython-busdevice +adafruit-circuitpython-pixelbuf