diff --git a/adafruit_is31fl3731/__init__.py b/adafruit_is31fl3731/__init__.py index 49290a4..01ae103 100644 --- a/adafruit_is31fl3731/__init__.py +++ b/adafruit_is31fl3731/__init__.py @@ -47,7 +47,6 @@ https://github.com/adafruit/circuitpython/releases """ - # imports import math import time @@ -55,6 +54,29 @@ from adafruit_bus_device.i2c_device import I2CDevice +try: + import typing + import busio + from circuitpython_typing import TypeAlias, Union + from circuitpython_typing import ( + WriteableBuffer, + ReadableBuffer, + ) # Import ReadableBuffer here + + from typing import ( + TYPE_CHECKING, + List, + Tuple, + Optional, + Iterable, + ) + + from PIL import Image + +except ImportError as e: + pass + + __version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_IS31FL3731.git" @@ -89,17 +111,25 @@ class IS31FL3731: :param ~busio.I2C i2c: the connected i2c bus i2c_device :param int address: the device address; defaults to 0x74 + :param Iterable frames: list of frame indexes to use. int's 0-7. """ - width = 16 - height = 9 + width: int = 16 + height: int = 9 - def __init__(self, i2c, address=0x74, frames=None): + def __init__( + self, + i2c: busio.I2C, + frames: Optional[Iterable] = None, + address: int = 0x74, + ): self.i2c_device = I2CDevice(i2c, address) self._frame = None self._init(frames=frames) - def _i2c_read_reg(self, reg, result): + def _i2c_read_reg( + self, reg: Optional[int] = None, result: Optional[WriteableBuffer] = None + ) -> Optional[WriteableBuffer]: # Read a buffer of data from the specified 8-bit I2C register address. # The provided result parameter will be filled to capacity with bytes # of data read from the register. @@ -108,25 +138,32 @@ def _i2c_read_reg(self, reg, result): return result return None - def _i2c_write_reg(self, reg, data): + def _i2c_write_reg( + self, reg: Optional[int] = None, data: Optional[ReadableBuffer] = None + ) -> None: # Write a contiguous block of data (bytearray) starting at the # specified I2C register address (register passed as argument). self._i2c_write_block(bytes([reg]) + data) - def _i2c_write_block(self, data): + def _i2c_write_block(self, data: Optional[ReadableBuffer]) -> None: # Write a buffer of data (byte array) to the specified I2C register # address. with self.i2c_device as i2c: i2c.write(data) - def _bank(self, bank=None): + def _bank(self, bank: Optional[int] = None) -> Optional[int]: if bank is None: result = bytearray(1) return self._i2c_read_reg(_BANK_ADDRESS, result)[0] self._i2c_write_reg(_BANK_ADDRESS, bytearray([bank])) return None - def _register(self, bank, register, value=None): + def _register( + self, + bank: Optional[int] = None, + register: Optional[int] = None, + value: Optional[int] = None, + ) -> Optional[int]: self._bank(bank) if value is None: result = bytearray(1) @@ -134,10 +171,11 @@ def _register(self, bank, register, value=None): self._i2c_write_reg(register, bytearray([value])) return None - def _mode(self, mode=None): + def _mode(self, mode: Optional[int] = None) -> int: + """Function for setting _register mode""" return self._register(_CONFIG_BANK, _MODE_REGISTER, mode) - def _init(self, frames=None): + def _init(self, frames: Iterable) -> None: self.sleep(True) # Clear config; sets to Picture Mode, no audio sync, maintains sleep self._bank(_CONFIG_BANK) @@ -154,13 +192,13 @@ def _init(self, frames=None): self._frame = 0 # To match config bytes above self.sleep(False) - def reset(self): + def reset(self) -> None: """Kill the display for 10MS""" self.sleep(True) time.sleep(0.01) # 10 MS pause to reset. self.sleep(False) - def sleep(self, value): + def sleep(self, value: bool) -> Optional[int]: """ Set the Software Shutdown Register bit @@ -168,7 +206,12 @@ def sleep(self, value): """ return self._register(_CONFIG_BANK, _SHUTDOWN_REGISTER, not value) - def autoplay(self, delay=0, loops=0, frames=0): + def autoplay( + self, + delay: int = 0, + loops: int = 0, + frames: int = 0, + ) -> None: """ Start autoplay @@ -190,15 +233,20 @@ def autoplay(self, delay=0, loops=0, frames=0): self._register(_CONFIG_BANK, _AUTOPLAY2_REGISTER, delay % 64) self._mode(_AUTOPLAY_MODE | self._frame) - def fade(self, fade_in=None, fade_out=None, pause=0): + def fade( + self, + fade_in: Optional[int] = None, + fade_out: Optional[int] = None, + pause: int = 0, + ) -> int: """ Start and stop the fade feature. If both fade_in and fade_out are None (the default), the breath feature is used for fading. if fade_in is None, then fade_in = fade_out. If fade_out is None, then fade_out = fade_in - :param fade_in: positive number; 0->100 - :param fade-out: positive number; 0->100 - :param pause: breath register 2 pause value + :param fade_in: int positive number; 0->100 + :param fade-out: int positive number; 0->100 + :param pause: int breath register 2 pause value """ if fade_in is None and fade_out is None: self._register(_CONFIG_BANK, _BREATH2_REGISTER, 0) @@ -223,12 +271,12 @@ def fade(self, fade_in=None, fade_out=None, pause=0): self._register(_CONFIG_BANK, _BREATH1_REGISTER, fade_out << 4 | fade_in) self._register(_CONFIG_BANK, _BREATH2_REGISTER, 1 << 4 | pause) - def frame(self, frame=None, show=True): + def frame(self, frame: Optional[int] = None, show: bool = True) -> Optional[int]: """ Set the current frame - :param frame: frame number; 0-7 or None. If None function returns current frame - :param show: True to show the frame; False to not show. + :param frame: int frame number; 0-7 or None. If None function returns current frame + :param show: bool True to show the frame; False to not show. """ if frame is None: return self._frame @@ -239,11 +287,17 @@ def frame(self, frame=None, show=True): self._register(_CONFIG_BANK, _FRAME_REGISTER, frame) return None - def audio_sync(self, value=None): + def audio_sync(self, value: Optional[int]) -> Optional[int]: """Set the audio sync feature register""" return self._register(_CONFIG_BANK, _AUDIOSYNC_REGISTER, value) - def audio_play(self, sample_rate, audio_gain=0, agc_enable=False, agc_fast=False): + def audio_play( + self, + sample_rate: int, + audio_gain: int = 0, + agc_enable: bool = False, + agc_fast: bool = False, + ) -> None: """Controls the audio play feature""" if sample_rate == 0: self._mode(_PICTURE_MODE) @@ -262,7 +316,7 @@ def audio_play(self, sample_rate, audio_gain=0, agc_enable=False, agc_fast=False ) self._mode(_AUDIOPLAY_MODE) - def blink(self, rate=None): + def blink(self, rate: Optional[int] = None) -> Optional[int]: """Updates the blink register""" # pylint: disable=no-else-return # This needs to be refactored when it can be tested @@ -275,13 +329,18 @@ def blink(self, rate=None): self._register(_CONFIG_BANK, _BLINK_REGISTER, rate & 0x07 | 0x08) return None - def fill(self, color=None, blink=None, frame=None): + def fill( + self, + color: Optional[int] = None, + frame: Optional[int] = None, + blink: bool = False, + ): """ Fill the display with a brightness level :param color: brightness 0->255 - :param blink: True if blinking is required - :param frame: which frame to fill 0->7 + :param blink: bool True to blink + :param frame: int the frame to set the pixel, default 0 """ if frame is None: frame = self._frame @@ -301,35 +360,72 @@ def fill(self, color=None, blink=None, frame=None): # This function must be replaced for each board @staticmethod - def pixel_addr(x, y): + def pixel_addr(x: int, y: int) -> int: """Calulate the offset into the device array for x,y pixel""" return x + y * 16 # pylint: disable-msg=too-many-arguments - def pixel(self, x, y, color=None, blink=None, frame=None): + def pixel( + self, + x: int, + y: int, + color: Optional[int] = None, + frame: Optional[int] = None, + blink: bool = False, + rotate: int = 0, + ) -> Optional[int]: """ - Blink or brightness for x-, y-pixel - - :param x: horizontal pixel position - :param y: vertical pixel position - :param color: brightness value 0->255 - :param blink: True to blink - :param frame: the frame to set the pixel + Matrix display configuration + + :param x: int horizontal pixel position + :param y: int vertical pixel position + :param color: int brightness value 0->255 + :param blink: bool True to blink + :param frame: int the frame to set the pixel, default 0 + :param rotate: int display rotation (0, 90, 180, 270) """ - if not 0 <= x <= self.width: - return None - if not 0 <= y <= self.height: - return None - pixel = self.pixel_addr(x, y) + # pylint: disable=too-many-branches + + if rotate not in (0, 90, 180, 270): + raise ValueError("Rotation must be 0, 90, 180, or 270 degrees") + + if rotate == 0: + check_x = 0 <= x <= self.width + check_y = 0 <= y <= self.height + if not (check_x and check_y): + return None + pixel = self.pixel_addr(x, y) + elif rotate == 90: + check_x = 0 <= y <= self.width + check_y = 0 <= x <= self.height + if not (check_x and check_y): + return None + pixel = self.pixel_addr(y, self.height - x - 1) + elif rotate == 180: + check_x = 0 <= x <= self.width + check_y = 0 <= y <= self.height + if not (check_x and check_y): + return None + pixel = self.pixel_addr(self.width - x - 1, self.height - y - 1) + elif rotate == 270: + check_x = 0 <= y <= self.width + check_y = 0 <= x <= self.height + if not (check_x and check_y): + return None + pixel = self.pixel_addr(self.width - y - 1, x) + if color is None and blink is None: return self._register(self._frame, pixel) + # frames other than 0 only used in animation. allow None. if frame is None: frame = self._frame + # Brightness if color is not None: if not 0 <= color <= 255: - raise ValueError("Color out of range") + raise ValueError("Brightness or Color out of range (0-255)") self._register(frame, _COLOR_OFFSET + pixel, color) - if blink is not None: + # Blink works but not well while animated + if blink: addr, bit = divmod(pixel, 8) bits = self._register(frame, _BLINK_OFFSET + addr) if blink: @@ -341,13 +437,15 @@ def pixel(self, x, y, color=None, blink=None, frame=None): # pylint: enable-msg=too-many-arguments - def image(self, img, blink=None, frame=None): + def image( + self, img: Image, frame: Optional[int] = None, blink: bool = False + ) -> None: """Set buffer to value of Python Imaging Library image. The image should be in 8-bit mode (L) and a size equal to the display size. :param img: Python Imaging Library image :param blink: True to blink - :param frame: the frame to set the image + :param frame: the frame to set the image, default 0 """ if img.mode != "L": raise ValueError("Image must be in mode L.") diff --git a/adafruit_is31fl3731/matrix.py b/adafruit_is31fl3731/matrix.py index 93b3034..73a2409 100644 --- a/adafruit_is31fl3731/matrix.py +++ b/adafruit_is31fl3731/matrix.py @@ -29,15 +29,26 @@ # imports from . import IS31FL3731 +try: + from typing import Optional + + try: + from PIL import Image + except ImportError: + # placeholder if PIL unavailable + Image = None +except ImportError as e: + pass + class Matrix(IS31FL3731): - """Supports the Charlieplexed feather wing""" + """Charlieplexed Featherwing & IS31FL3731 I2C Modules""" - width = 16 - height = 9 + width: int = 16 + height: int = 9 @staticmethod - def pixel_addr(x, y): + def pixel_addr(x: int, y: int) -> int: """Calulate the offset into the device array for x,y pixel""" return x + y * 16 @@ -48,7 +59,7 @@ def pixel_addr(x, y): # for animation. Buffering the full matrix for a quick write is not a # memory concern here, as by definition this method is used with PIL # images; we're not running on a RAM-constrained microcontroller. - def image(self, img, blink=None, frame=None): + def image(self, img: Image, frame: Optional[int] = None, blink: bool = False): """Set buffer to value of Python Imaging Library image. The image should be in 8-bit mode (L) and a size equal to the display size. @@ -61,9 +72,7 @@ def image(self, img, blink=None, frame=None): raise ValueError("Image must be in mode L.") if img.size[0] != self.width or img.size[1] != self.height: raise ValueError( - "Image must be same dimensions as display ({0}x{1}).".format( - self.width, self.height - ) + f"Image must be same dimensions as display {self.width}x{self.height}" ) # Frame-select and then write pixel data in one big operation @@ -74,6 +83,6 @@ def image(self, img, blink=None, frame=None): # pixel array or invoke pixel_addr(). self._i2c_write_block(bytes([0x24]) + img.tobytes()) # Set or clear blink state if requested, for all pixels at once - if blink is not None: + if blink: # 0x12 is _BLINK_OFFSET in __init__.py self._i2c_write_block(bytes([0x12] + [1 if blink else 0] * 18)) diff --git a/examples/is31fl3731_16x9_charlieplexed_pwm.py b/examples/is31fl3731_16x9_charlieplexed_pwm.py new file mode 100644 index 0000000..6c2878e --- /dev/null +++ b/examples/is31fl3731_16x9_charlieplexed_pwm.py @@ -0,0 +1,49 @@ +# SPDX-FileCopyrightText: 2024 DJDevon3 +# SPDX-License-Identifier: MIT +""" Adafruit 16x9 Charlieplexed PWM LED Matrix Example """ +# pylint: disable=import-error +import board +import adafruit_framebuf + +from adafruit_is31fl3731.matrix import Matrix as Display + +# Uncomment for Pi Pico +# import busio +# i2c = busio.I2C(board.GP21, board.GP20) + +i2c = board.STEMMA_I2C() +display = Display(i2c, address=0x74) + +PIXEL_ROTATION = 0 # display rotation (0,90,180,270) +PIXEL_BRIGHTNESS = 20 # values (0-255) +PIXEL_BLINK = False # blink entire display + +TEXT = "Hello World!" # Scrolling marquee text + +print(f"Display Dimensions: {display.width}x{display.height}") +print(f"Text: {TEXT}") + +# Create a framebuffer for our display +buf = bytearray(32) # 2 bytes tall x 16 wide = 32 bytes (9 bits is 2 bytes) +buffer = adafruit_framebuf.FrameBuffer( + buf, display.width, display.height, adafruit_framebuf.MVLSB +) + +FRAME = 0 # start with frame 0 +while True: + # Looping marquee + for i in range(len(TEXT) * 9): + buffer.fill(0) + buffer.text(TEXT, -i + display.width, 0, color=1) + display.frame(FRAME, show=False) + display.fill(0) + for x in range(display.width): + # using the FrameBuffer text result + bite = buf[x] + for y in range(display.height): + bit = 1 << y & bite + # if bit > 0 then set the pixel brightness + if bit: + display.pixel( + x, y, PIXEL_BRIGHTNESS, blink=PIXEL_BLINK, rotate=PIXEL_ROTATION + ) diff --git a/optional_requirements.txt b/optional_requirements.txt index dcabec4..afdd721 100644 --- a/optional_requirements.txt +++ b/optional_requirements.txt @@ -3,3 +3,4 @@ # SPDX-License-Identifier: Unlicense adafruit-circuitpython-framebuf +pillow