From a02d62fdd979483359e389f02d846d851197436f Mon Sep 17 00:00:00 2001 From: Phillip Burgess Date: Mon, 21 Mar 2022 17:24:20 -0700 Subject: [PATCH 1/3] Faster _init(), much faster image() for Matrix class --- adafruit_is31fl3731/__init__.py | 43 ++++++++++++++++------------- adafruit_is31fl3731/matrix.py | 48 +++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 18 deletions(-) diff --git a/adafruit_is31fl3731/__init__.py b/adafruit_is31fl3731/__init__.py index 2d65f4e..5a285a4 100644 --- a/adafruit_is31fl3731/__init__.py +++ b/adafruit_is31fl3731/__init__.py @@ -92,11 +92,11 @@ class IS31FL3731: width = 16 height = 9 - def __init__(self, i2c, address=0x74): + def __init__(self, i2c, address=0x74, frames=None): self.i2c = i2c self.address = address self._frame = None - self._init() + self._init(frames=frames) def _i2c_read_reg(self, reg, result): # Read a buffer of data from the specified 8-bit I2C register address. @@ -111,19 +111,21 @@ def _i2c_read_reg(self, reg, result): self.i2c.unlock() return None - def _i2c_write_reg(self, reg, data): - # Write a buffer of data (byte array) to the specified I2C register - # address. + def _i2c_write_block(self, data): + # Writes a contiguous block of data (bytearray) where the first byte + # is the starting I2C register address (register is not an argument). while not self.i2c.try_lock(): pass try: - buf = bytearray(1) - buf[0] = reg - buf.extend(data) - self.i2c.writeto(self.address, buf) + self.i2c.writeto(self.address, data) finally: self.i2c.unlock() + def _i2c_write_reg(self, reg, data): + # 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 _bank(self, bank=None): if bank is None: result = bytearray(1) @@ -142,16 +144,21 @@ def _register(self, bank, register, value=None): def _mode(self, mode=None): return self._register(_CONFIG_BANK, _MODE_REGISTER, mode) - def _init(self): + def _init(self, frames=None): self.sleep(True) - time.sleep(0.01) # 10 MS pause to reset. - self._mode(_PICTURE_MODE) - self.frame(0) - for frame in range(8): - self.fill(0, False, frame=frame) - for col in range(18): - self._register(frame, _ENABLE_OFFSET + col, 0xFF) - self.audio_sync(False) + # Clear config; sets to Picture Mode, no audio sync, maintains sleep + self._bank(_CONFIG_BANK) + self._i2c_write_block(bytes([0] * 14)) + enable_data = bytes([_ENABLE_OFFSET] + [255] * 18) + fill_data = bytearray([0] * 25) + # Initialize requested frames, or all 8 if unspecified + for frame in frames if frames else range(8): + self._bank(frame) + self._i2c_write_block(enable_data) # Set all enable bits + for row in range(6): # Barebones quick fill() w/0 + fill_data[0] = _COLOR_OFFSET + row * 24 + self._i2c_write_block(fill_data) + self._frame = 0 # To match config bytes above self.sleep(False) def reset(self): diff --git a/adafruit_is31fl3731/matrix.py b/adafruit_is31fl3731/matrix.py index 94a2c9e..2f51853 100644 --- a/adafruit_is31fl3731/matrix.py +++ b/adafruit_is31fl3731/matrix.py @@ -40,3 +40,51 @@ class Matrix(IS31FL3731): def pixel_addr(x, y): """Calulate the offset into the device array for x,y pixel""" return x + y * 16 + + # This takes precedence over image() in __init__ and is tuned for the + # Matrix class. Some shortcuts can be taken because matrix layout is + # very straightforward, and a few large write operations are used + # rather than pixel-by-pixel writes, yielding significant speed gains + # 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): + """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 + """ + if img.mode != "L": + 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 + ) + ) + + # Grab all the pixels from the image, faster than getpixel. + pixels = img.load() + + # Convert pixels to bytearray, iterating through each pixel + # sequentially, row-major. We can do this because the matrix + # layout is known linear; don't need to go through pixel_addr(). + buffer = bytearray(16 * 9 + 1) # +1 for address + buffer[0] = 0x24 # _COLOR_OFFSET in __init__.py + idx = 1 # Pixel data starts at buffer[1] + for y in range(self.height): + for x in range(self.width): + buffer[idx] = pixels[(x, y)] + idx += 1 + + # Frame-select and then write pixel data in one big operation + if frame is not None: + self._bank(frame) + self._i2c_write_block(buffer) + # Set or clear blink state if requested, for all pixels at once + if blink is not None: + # 0x12 is _BLINK_OFFSET in __init__.py + self._i2c_write_block(bytes([0x12] + [1 if blink else 0] * 18)) From 99acfeef0d32eac966b1f3b7e7e07a011555c8c2 Mon Sep 17 00:00:00 2001 From: Phillip Burgess Date: Mon, 21 Mar 2022 17:53:11 -0700 Subject: [PATCH 2/3] black format --- adafruit_is31fl3731/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adafruit_is31fl3731/__init__.py b/adafruit_is31fl3731/__init__.py index 5a285a4..55b53df 100644 --- a/adafruit_is31fl3731/__init__.py +++ b/adafruit_is31fl3731/__init__.py @@ -154,8 +154,8 @@ def _init(self, frames=None): # Initialize requested frames, or all 8 if unspecified for frame in frames if frames else range(8): self._bank(frame) - self._i2c_write_block(enable_data) # Set all enable bits - for row in range(6): # Barebones quick fill() w/0 + self._i2c_write_block(enable_data) # Set all enable bits + for row in range(6): # Barebones quick fill() w/0 fill_data[0] = _COLOR_OFFSET + row * 24 self._i2c_write_block(fill_data) self._frame = 0 # To match config bytes above From 1f3bf4f0b7aa66ca089e2491cb45cfb7a281ed4a Mon Sep 17 00:00:00 2001 From: Phillip Burgess Date: Tue, 22 Mar 2022 12:30:04 -0700 Subject: [PATCH 3/3] Further simplify the Matrix image() method --- adafruit_is31fl3731/matrix.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/adafruit_is31fl3731/matrix.py b/adafruit_is31fl3731/matrix.py index 2f51853..93b3034 100644 --- a/adafruit_is31fl3731/matrix.py +++ b/adafruit_is31fl3731/matrix.py @@ -66,24 +66,13 @@ def image(self, img, blink=None, frame=None): ) ) - # Grab all the pixels from the image, faster than getpixel. - pixels = img.load() - - # Convert pixels to bytearray, iterating through each pixel - # sequentially, row-major. We can do this because the matrix - # layout is known linear; don't need to go through pixel_addr(). - buffer = bytearray(16 * 9 + 1) # +1 for address - buffer[0] = 0x24 # _COLOR_OFFSET in __init__.py - idx = 1 # Pixel data starts at buffer[1] - for y in range(self.height): - for x in range(self.width): - buffer[idx] = pixels[(x, y)] - idx += 1 - # Frame-select and then write pixel data in one big operation if frame is not None: self._bank(frame) - self._i2c_write_block(buffer) + # We can safely reduce the image to a "flat" byte sequence because + # the matrix layout is known linear; no need to go through a 2D + # 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: # 0x12 is _BLINK_OFFSET in __init__.py