Skip to content

Add RGB color ordering a la Arduino library, FrameBuffer copy via image() #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Oct 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
239 changes: 161 additions & 78 deletions adafruit_is31fl3741/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

"""

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
Expand Down Expand Up @@ -51,10 +52,13 @@

class IS31FL3741:
"""
The IS31FL3741 is an abstract class contain the main function related to this chip.
Each board needs to define width, height and pixel_addr.
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 ~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
Expand All @@ -64,9 +68,6 @@ class IS31FL3741:
MemoryError if allocation fails.
"""

width = 13
height = 9

_page_reg = UnaryStruct(_IS3741_COMMANDREGISTER, "<B")
_lock_reg = UnaryStruct(_IS3741_COMMANDREGISTERLOCK, "<B")
_id_reg = UnaryStruct(_IS3741_IDREGISTER, "<B")
Expand Down Expand Up @@ -104,18 +105,18 @@ def unlock(self):
self._lock_reg = 0xC5

def set_led_scaling(self, scale):
"""Set LED scaling.
"""Set scaling level for all LEDs.

param scale: The scale.
:param scale: Scaling level from 0 (off) to 255 (brightest).
"""
scalebuf = [scale] * 181
scalebuf[0] = 0
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(bytes(scalebuf))
i2c.write(scalebuf)
self.page = 3
with self.i2c_device as i2c:
i2c.write(bytes(scalebuf))
i2c.write(scalebuf, end=172) # 2nd page is smaller

@property
def global_current(self):
Expand Down Expand Up @@ -173,75 +174,26 @@ def __getitem__(self, led):
return self._buf[1]

def __setitem__(self, led, pwm):
if not 0 <= led <= 350:
raise ValueError("LED must be 0 ~ 350")
if not 0 <= pwm <= 255:
raise ValueError("PWM must be 0 ~ 255")
# print(led, pwm)
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
else:
if led < 180:
self.page = 0
self._buf[0] = led
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:
self.page = 1
self._buf[0] = led - 180
self._buf[1] = pwm
with self.i2c_device as i2c:
i2c.write(self._buf)

# This function must be replaced for each board
@staticmethod
def pixel_addrs(x, y):
"""Calulate the offset into the device array for x,y pixel"""
raise NotImplementedError("Supported in subclasses only")

# pylint: disable-msg=too-many-arguments
def pixel(self, x, y, color=None):
"""
Color of for x-, y-pixel

:param x: horizontal pixel position
:param y: vertical pixel position
:param color: hex color value 0x000000 to 0xFFFFFF to set,
or None to return current pixel value
"""

if 0 <= x < self.width and 0 <= y < self.height: # Clip
addrs = self.pixel_addrs(x, y) # LED indices
# print(addrs)
if color is None: # Return current pixel color if unspecified
return (self[addrs[0]] << 16) | (self[addrs[1]] << 8) | self[addrs[2]]
self[addrs[0]] = (color >> 16) & 0xFF
self[addrs[1]] = (color >> 8) & 0xFF
self[addrs[2]] = color & 0xFF
return None

# pylint: enable-msg=too-many-arguments

def image(self, img):
"""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
"""
if img.mode != "RGB":
raise ValueError("Image must be in mode RGB.")
imwidth, imheight = img.size
if imwidth != self.width or imheight != 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()

# Iterate through the pixels
for x in range(self.width): # yes this double loop is slow,
for y in range(self.height): # but these displays are small!
self.pixel(x, y, pixels[(x, y)])
raise ValueError("PWM must be 0 ~ 255")
else:
raise ValueError("LED must be 0 ~ 350")

def show(self):
"""Issue in-RAM pixel data to device. No effect if pixels are
Expand All @@ -266,3 +218,134 @@ def show(self):
self._pixel_buffer[180] = 0
i2c.write(self._pixel_buffer, start=180, end=352)
self._pixel_buffer[180] = save


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


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,
width,
height,
address=_IS3741_ADDR_DEFAULT,
allocate=NO_BUFFER,
order=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
@staticmethod
def pixel_addrs(x, y):
"""Calculate a device-specific LED offset for an X,Y 2D pixel."""
raise NotImplementedError("Supported in subclasses only")

def fill(self, color=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

def pixel(self, x, y, color=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

def image(self, img):
"""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)])
Loading