# 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