Source code for adafruit_is31fl3741

# SPDX-FileCopyrightText: Tony DiCola 2017 for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""
`adafruit_is31fl3741`
====================================================

CircuitPython driver for the IS31FL3741 RGB Matrix IC.

Base library.

* Author(s): Ladyada

Implementation Notes
--------------------

**Hardware:**

**Software and Dependencies:**

* Adafruit CircuitPython firmware for the supported boards:
  https://github.com/adafruit/circuitpython/releases

"""

from sys import implementation
from adafruit_bus_device import i2c_device
from adafruit_register.i2c_struct import ROUnaryStruct, UnaryStruct
from adafruit_register.i2c_bit import RWBit

try:
    # Used only for typing
    from typing import Optional, Tuple, Union  # pylint: disable=unused-import
    from circuitpython_typing.pil import Image
    from circuitpython_typing import ReadableBuffer
    from adafruit_framebuf import FrameBuffer
    import busio
except ImportError:
    pass

__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_IS31FL3741.git"

_IS3741_ADDR_DEFAULT = 0x30

_IS3741_COMMANDREGISTER = 0xFD
_IS3741_COMMANDREGISTERLOCK = 0xFE
_IS3741_INTMASKREGISTER = 0xF0
_IS3741_INTSTATUSREGISTER = 0xF1
_IS3741_IDREGISTER = 0xFC

_IS3741_FUNCREG_CONFIG = 0x00
_IS3741_FUNCREG_GCURRENT = 0x01
_IS3741_FUNCREG_RESET = 0x3F

# Buffer allocation behaviors passed to constructor
NO_BUFFER = 0x00  # DO NOT buffer pixel data, write pixels as needed
PREFER_BUFFER = 0x01  # OPTIONALLY buffer pixel data, RAM permitting
MUST_BUFFER = 0x02  # MUST buffer pixel data, else throw MemoryError


[docs] class IS31FL3741: """ The IS31FL3741 is an abstract class containing the main function related to this chip. It focuses on lowest-level I2C operations and chip registers, and has no concept of a 2D graphics coordinate system, nor of RGB colors (subclasses provide these). It is linear and monochromatic. :param ~adafruit_bus_device.i2c_device i2c_device: the connected i2c bus i2c_device :param address: the device address; defaults to 0x30 :param allocate: buffer allocation strategy: NO_BUFFER = pixels are always sent to device as they're set. PREFER_BUFFER = RAM permitting, buffer pixels in RAM, updating device only when show() is called, but fall back on NO_BUFFER behavior. MUST_BUFFER = buffer pixels in RAM, throw MemoryError if allocation fails. """ _page_reg = UnaryStruct(_IS3741_COMMANDREGISTER, "<B") _lock_reg = UnaryStruct(_IS3741_COMMANDREGISTERLOCK, "<B") _id_reg = UnaryStruct(_IS3741_IDREGISTER, "<B") _config_reg = UnaryStruct(_IS3741_FUNCREG_CONFIG, "<B") _gcurrent_reg = UnaryStruct(_IS3741_FUNCREG_GCURRENT, "<B") _reset_reg = UnaryStruct(_IS3741_FUNCREG_RESET, "<B") _shutdown_bit = RWBit(_IS3741_FUNCREG_CONFIG, 0) _pixel_buffer = None def __init__( self, i2c: busio.I2C, address: int = _IS3741_ADDR_DEFAULT, allocate: int = NO_BUFFER, ): if allocate >= PREFER_BUFFER: try: # Pixel buffer intentionally has an extra item at the start # (value of 0) so we can i2c.write() from the buffer directly # (don't need a temp/copy buffer to pre-pend the register # address). self._pixel_buffer = bytearray(352) except MemoryError: if allocate == MUST_BUFFER: raise self.i2c_device = i2c_device.I2CDevice(i2c, address) if self._id_reg != 2 * address: raise AttributeError("Cannot find a IS31FL3741 at address 0x", address) self._buf = bytearray(2) self._page = None self.reset()
[docs] def reset(self) -> None: """Reset""" self.page = 4 self._reset_reg = 0xAE
[docs] def unlock(self) -> None: """Unlock""" self._lock_reg = 0xC5
[docs] def set_led_scaling(self, scale: int) -> None: """Set scaling level for all LEDs. :param scale: Scaling level from 0 (off) to 255 (brightest). """ scalebuf = bytearray([scale] * 181) # 180 bytes + 1 for reg addr scalebuf[0] = 0 # Initial register address self.page = 2 with self.i2c_device as i2c: i2c.write(scalebuf) self.page = 3 with self.i2c_device as i2c: i2c.write(scalebuf, end=172) # 2nd page is smaller
@property def global_current(self) -> int: """Global current""" self.page = 4 return self._gcurrent_reg @global_current.setter def global_current(self, current: int) -> None: self.page = 4 self._gcurrent_reg = current @property def enable(self) -> bool: """Enable""" self.page = 4 return self._shutdown_bit @enable.setter def enable(self, enable: bool) -> None: self.page = 4 self._shutdown_bit = enable @property def page(self) -> Union[int, None]: """Page""" return self._page @page.setter def page(self, page_value: int) -> None: if page_value == self._page: return # already set if page_value > 4: raise ValueError("Page must be 0 ~ 4") self._page = page_value # cache self.unlock() self._page_reg = page_value def __getitem__(self, led: int) -> int: if not 0 <= led <= 350: raise ValueError("LED must be 0 ~ 350") if self._pixel_buffer: return self._pixel_buffer[1 + led] if led < 180: self.page = 0 self._buf[0] = led else: self.page = 1 self._buf[0] = led - 180 with self.i2c_device as i2c: i2c.write_then_readinto( self._buf, self._buf, out_start=0, out_end=1, in_start=1, in_end=2 ) return self._buf[1] def __setitem__(self, led: int, pwm: int) -> None: if self._pixel_buffer: # Buffered version doesn't require range checks -- # Python will throw its own IndexError/ValueError as needed. self._pixel_buffer[1 + led] = pwm elif 0 <= led <= 350: if 0 <= pwm <= 255: # print(led, pwm) if led < 180: self.page = 0 self._buf[0] = led else: self.page = 1 self._buf[0] = led - 180 self._buf[1] = pwm with self.i2c_device as i2c: i2c.write(self._buf) else: raise ValueError("PWM must be 0 ~ 255") else: raise ValueError("LED must be 0 ~ 350")
[docs] def show(self) -> None: """Issue in-RAM pixel data to device. No effect if pixels are unbuffered. """ if self._pixel_buffer: self.page = 0 with self.i2c_device as i2c: # _pixel_buffer[0] is always 0! (First register addr) i2c.write(self._pixel_buffer, start=0, end=181) self.page = 1 with self.i2c_device as i2c: # In order to write from pixel buffer directly (without a # whole extra temp buffer), element 180 is saved in a temp var # and replaced with 0 (representing the first regisyer addr on # page 1), then we can i2c.write() directly from that position # in the buffer. Element 180 is restored afterward. This is # the same strategy as used in the Arduino library. # 'end' below is 352 (not 351) because of the extra byte at # the start of the pixel buffer. save = self._pixel_buffer[180] self._pixel_buffer[180] = 0 i2c.write(self._pixel_buffer, start=180, end=352) self._pixel_buffer[180] = save
[docs] def write(self, mapping: Tuple, buffer: ReadableBuffer) -> None: """ Write buf out on the I2C bus to the IS31FL3741. :param mapping: map the pixels in the buffer to the order addressed by the driver chip :param buffer: The bytes to clock out. No assumption is made about color order :return: None """ for pos, data in enumerate(buffer): if mapping[pos] != 65535: self[mapping[pos]] = data self.show()
IS3741_RGB = (0 << 4) | (1 << 2) | (2) # Encode as R,G,B IS3741_RBG = (0 << 4) | (2 << 2) | (1) # Encode as R,B,G IS3741_GRB = (1 << 4) | (0 << 2) | (2) # Encode as G,R,B IS3741_GBR = (2 << 4) | (0 << 2) | (1) # Encode as G,B,R IS3741_BRG = (1 << 4) | (2 << 2) | (0) # Encode as B,R,G IS3741_BGR = (2 << 4) | (1 << 2) | (0) # Encode as B,G,R
[docs] class IS31FL3741_colorXY(IS31FL3741): """ Class encompassing IS31FL3741 and a minimal layer for RGB color 2D pixel operations (base class is hardware- and register-centric and lacks these concepts). Specific boards like the QT matrix or EyeLights glasses then subclass this. In theory, a companion monochrome XY class could be separately implemented in the future if required for anything. Mostly though, this is about providing a place for common RGB matrix functions like fill() that then work across all such devices. :param ~adafruit_bus_device.i2c_device i2c_device: the connected i2c bus i2c_device :param width: Matrix width in pixels. :param height: Matrix height in pixels. :param address: the device address; defaults to 0x30 :param allocate: buffer allocation strategy: NO_BUFFER = pixels are always sent to device as they're set. PREFER_BUFFER = RAM permitting, buffer pixels in RAM, updating device only when show() is called, but fall back on NO_BUFFER behavior. MUST_BUFFER = buffer pixels in RAM, throw MemoryError if allocation fails. :param order: Pixel RGB color order, one of the IS3741_* color types above. Default is IS3741_BGR. """ # pylint: disable-msg=too-many-arguments def __init__( self, i2c: busio.I2C, width: int, height: int, address: int = _IS3741_ADDR_DEFAULT, allocate: int = NO_BUFFER, order: int = IS3741_BGR, ): super().__init__(i2c, address=address, allocate=allocate) self.order = order self.width = width self.height = height self.r_offset = (order >> 4) & 3 self.g_offset = (order >> 2) & 3 self.b_offset = order & 3 # pylint: enable-msg=too-many-arguments # This function must be replaced for each board
[docs] @staticmethod def pixel_addrs(x: int, y: int) -> Tuple[int, ...]: """Calculate a device-specific LED offset for an X,Y 2D pixel.""" raise NotImplementedError("Supported in subclasses only")
[docs] def fill(self, color: int = 0) -> None: """Set all pixels to a given RGB color. :param color: Packed 24-bit color value (0xRRGGBB). """ red = (color >> 16) & 0xFF green = (color >> 8) & 0xFF blue = color & 0xFF for y in range(self.height): for x in range(self.width): addrs = self.pixel_addrs(x, y) self[addrs[self.r_offset]] = red self[addrs[self.g_offset]] = green self[addrs[self.b_offset]] = blue
[docs] def pixel(self, x: int, y: int, color: Optional[int] = None) -> Union[int, None]: """ Set or retrieve RGB color of pixel at position (X,Y). :param x: Horizontal pixel position. :param y: Vertical pixel position. :param color: If setting, a packed 24-bit color value (0xRRGGBB). If getting, either None or leave off this argument. :returns: If setting, returns None. If getting, returns a packed 24-bit color value (0xRRGGBB). """ if 0 <= x < self.width and 0 <= y < self.height: # Clip addrs = self.pixel_addrs(x, y) # LED indices # print(addrs) if color is not None: self[addrs[self.r_offset]] = (color >> 16) & 0xFF self[addrs[self.g_offset]] = (color >> 8) & 0xFF self[addrs[self.b_offset]] = color & 0xFF else: # Return current pixel color if unspecified return ( (self[addrs[self.r_offset]] << 16) | (self[addrs[self.g_offset]] << 8) | self[addrs[self.b_offset]] ) return None
[docs] def image(self, img: Union[FrameBuffer, Image]) -> None: """Copy an in-memory image to the LED matrix. Image should be in 24-bit format (e.g. "RGB888") and dimensions should match matrix, this isn't super robust yet or anything. :param img: Source image -- either a FrameBuffer object if running CircuitPython, or PIL image if running CPython w/Python Imaging Lib. """ if implementation.name == "circuitpython": for y in range(self.height): for x in range(self.width): self.pixel(x, y, img.pixel(x, y)) else: if img.mode != "RGB": raise ValueError("Image must be in mode RGB.") 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 ) ) # Iterate X/Y through all image pixels pixels = img.load() # Grab all pixels, faster than getpixel on each for y in range(self.height): for x in range(self.width): self.pixel(x, y, pixels[(x, y)])
def __len__(self): return self.width * self.height * 3