Skip to content

Commit 8b0a931

Browse files
Merge pull request #9 from adafruit/pb-color-order
Add RGB color ordering a la Arduino library, FrameBuffer copy via image()
2 parents e9ac189 + 6a2d9b5 commit 8b0a931

File tree

4 files changed

+390
-429
lines changed

4 files changed

+390
-429
lines changed

adafruit_is31fl3741/__init__.py

Lines changed: 161 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
2525
"""
2626

27+
from sys import implementation
2728
from adafruit_bus_device import i2c_device
2829
from adafruit_register.i2c_struct import ROUnaryStruct, UnaryStruct
2930
from adafruit_register.i2c_bit import RWBit
@@ -51,10 +52,13 @@
5152

5253
class IS31FL3741:
5354
"""
54-
The IS31FL3741 is an abstract class contain the main function related to this chip.
55-
Each board needs to define width, height and pixel_addr.
55+
The IS31FL3741 is an abstract class containing the main function related
56+
to this chip. It focuses on lowest-level I2C operations and chip
57+
registers, and has no concept of a 2D graphics coordinate system, nor of
58+
RGB colors (subclasses provide these). It is linear and monochromatic.
5659
57-
:param ~adafruit_bus_device.i2c_device i2c_device: the connected i2c bus i2c_device
60+
:param ~adafruit_bus_device.i2c_device i2c_device: the connected i2c bus
61+
i2c_device
5862
:param address: the device address; defaults to 0x30
5963
:param allocate: buffer allocation strategy: NO_BUFFER = pixels are always
6064
sent to device as they're set. PREFER_BUFFER = RAM
@@ -64,9 +68,6 @@ class IS31FL3741:
6468
MemoryError if allocation fails.
6569
"""
6670

67-
width = 13
68-
height = 9
69-
7071
_page_reg = UnaryStruct(_IS3741_COMMANDREGISTER, "<B")
7172
_lock_reg = UnaryStruct(_IS3741_COMMANDREGISTERLOCK, "<B")
7273
_id_reg = UnaryStruct(_IS3741_IDREGISTER, "<B")
@@ -104,18 +105,18 @@ def unlock(self):
104105
self._lock_reg = 0xC5
105106

106107
def set_led_scaling(self, scale):
107-
"""Set LED scaling.
108+
"""Set scaling level for all LEDs.
108109
109-
param scale: The scale.
110+
:param scale: Scaling level from 0 (off) to 255 (brightest).
110111
"""
111-
scalebuf = [scale] * 181
112-
scalebuf[0] = 0
112+
scalebuf = bytearray([scale] * 181) # 180 bytes + 1 for reg addr
113+
scalebuf[0] = 0 # Initial register address
113114
self.page = 2
114115
with self.i2c_device as i2c:
115-
i2c.write(bytes(scalebuf))
116+
i2c.write(scalebuf)
116117
self.page = 3
117118
with self.i2c_device as i2c:
118-
i2c.write(bytes(scalebuf))
119+
i2c.write(scalebuf, end=172) # 2nd page is smaller
119120

120121
@property
121122
def global_current(self):
@@ -173,75 +174,26 @@ def __getitem__(self, led):
173174
return self._buf[1]
174175

175176
def __setitem__(self, led, pwm):
176-
if not 0 <= led <= 350:
177-
raise ValueError("LED must be 0 ~ 350")
178-
if not 0 <= pwm <= 255:
179-
raise ValueError("PWM must be 0 ~ 255")
180-
# print(led, pwm)
181177
if self._pixel_buffer:
178+
# Buffered version doesn't require range checks --
179+
# Python will throw its own IndexError/ValueError as needed.
182180
self._pixel_buffer[1 + led] = pwm
183-
else:
184-
if led < 180:
185-
self.page = 0
186-
self._buf[0] = led
181+
elif 0 <= led <= 350:
182+
if 0 <= pwm <= 255:
183+
# print(led, pwm)
184+
if led < 180:
185+
self.page = 0
186+
self._buf[0] = led
187+
else:
188+
self.page = 1
189+
self._buf[0] = led - 180
190+
self._buf[1] = pwm
191+
with self.i2c_device as i2c:
192+
i2c.write(self._buf)
187193
else:
188-
self.page = 1
189-
self._buf[0] = led - 180
190-
self._buf[1] = pwm
191-
with self.i2c_device as i2c:
192-
i2c.write(self._buf)
193-
194-
# This function must be replaced for each board
195-
@staticmethod
196-
def pixel_addrs(x, y):
197-
"""Calulate the offset into the device array for x,y pixel"""
198-
raise NotImplementedError("Supported in subclasses only")
199-
200-
# pylint: disable-msg=too-many-arguments
201-
def pixel(self, x, y, color=None):
202-
"""
203-
Color of for x-, y-pixel
204-
205-
:param x: horizontal pixel position
206-
:param y: vertical pixel position
207-
:param color: hex color value 0x000000 to 0xFFFFFF to set,
208-
or None to return current pixel value
209-
"""
210-
211-
if 0 <= x < self.width and 0 <= y < self.height: # Clip
212-
addrs = self.pixel_addrs(x, y) # LED indices
213-
# print(addrs)
214-
if color is None: # Return current pixel color if unspecified
215-
return (self[addrs[0]] << 16) | (self[addrs[1]] << 8) | self[addrs[2]]
216-
self[addrs[0]] = (color >> 16) & 0xFF
217-
self[addrs[1]] = (color >> 8) & 0xFF
218-
self[addrs[2]] = color & 0xFF
219-
return None
220-
221-
# pylint: enable-msg=too-many-arguments
222-
223-
def image(self, img):
224-
"""Set buffer to value of Python Imaging Library image. The image should
225-
be in 8-bit mode (L) and a size equal to the display size.
226-
227-
:param img: Python Imaging Library image
228-
"""
229-
if img.mode != "RGB":
230-
raise ValueError("Image must be in mode RGB.")
231-
imwidth, imheight = img.size
232-
if imwidth != self.width or imheight != self.height:
233-
raise ValueError(
234-
"Image must be same dimensions as display ({0}x{1}).".format(
235-
self.width, self.height
236-
)
237-
)
238-
# Grab all the pixels from the image, faster than getpixel.
239-
pixels = img.load()
240-
241-
# Iterate through the pixels
242-
for x in range(self.width): # yes this double loop is slow,
243-
for y in range(self.height): # but these displays are small!
244-
self.pixel(x, y, pixels[(x, y)])
194+
raise ValueError("PWM must be 0 ~ 255")
195+
else:
196+
raise ValueError("LED must be 0 ~ 350")
245197

246198
def show(self):
247199
"""Issue in-RAM pixel data to device. No effect if pixels are
@@ -266,3 +218,134 @@ def show(self):
266218
self._pixel_buffer[180] = 0
267219
i2c.write(self._pixel_buffer, start=180, end=352)
268220
self._pixel_buffer[180] = save
221+
222+
223+
IS3741_RGB = (0 << 4) | (1 << 2) | (2) # Encode as R,G,B
224+
IS3741_RBG = (0 << 4) | (2 << 2) | (1) # Encode as R,B,G
225+
IS3741_GRB = (1 << 4) | (0 << 2) | (2) # Encode as G,R,B
226+
IS3741_GBR = (2 << 4) | (0 << 2) | (1) # Encode as G,B,R
227+
IS3741_BRG = (1 << 4) | (2 << 2) | (0) # Encode as B,R,G
228+
IS3741_BGR = (2 << 4) | (1 << 2) | (0) # Encode as B,G,R
229+
230+
231+
class IS31FL3741_colorXY(IS31FL3741):
232+
"""
233+
Class encompassing IS31FL3741 and a minimal layer for RGB color 2D
234+
pixel operations (base class is hardware- and register-centric and
235+
lacks these concepts). Specific boards like the QT matrix or EyeLights
236+
glasses then subclass this. In theory, a companion monochrome XY class
237+
could be separately implemented in the future if required for anything.
238+
Mostly though, this is about providing a place for common RGB matrix
239+
functions like fill() that then work across all such devices.
240+
241+
:param ~adafruit_bus_device.i2c_device i2c_device: the connected i2c bus
242+
i2c_device
243+
:param width: Matrix width in pixels.
244+
:param height: Matrix height in pixels.
245+
:param address: the device address; defaults to 0x30
246+
:param allocate: buffer allocation strategy: NO_BUFFER = pixels are always
247+
sent to device as they're set. PREFER_BUFFER = RAM
248+
permitting, buffer pixels in RAM, updating device only
249+
when show() is called, but fall back on NO_BUFFER
250+
behavior. MUST_BUFFER = buffer pixels in RAM, throw
251+
MemoryError if allocation fails.
252+
:param order: Pixel RGB color order, one of the IS3741_* color types
253+
above. Default is IS3741_BGR.
254+
"""
255+
256+
# pylint: disable-msg=too-many-arguments
257+
def __init__(
258+
self,
259+
i2c,
260+
width,
261+
height,
262+
address=_IS3741_ADDR_DEFAULT,
263+
allocate=NO_BUFFER,
264+
order=IS3741_BGR,
265+
):
266+
super().__init__(i2c, address=address, allocate=allocate)
267+
self.order = order
268+
self.width = width
269+
self.height = height
270+
self.r_offset = (order >> 4) & 3
271+
self.g_offset = (order >> 2) & 3
272+
self.b_offset = order & 3
273+
274+
# pylint: enable-msg=too-many-arguments
275+
276+
# This function must be replaced for each board
277+
@staticmethod
278+
def pixel_addrs(x, y):
279+
"""Calculate a device-specific LED offset for an X,Y 2D pixel."""
280+
raise NotImplementedError("Supported in subclasses only")
281+
282+
def fill(self, color=None):
283+
"""Set all pixels to a given RGB color.
284+
285+
:param color: Packed 24-bit color value (0xRRGGBB).
286+
"""
287+
red = (color >> 16) & 0xFF
288+
green = (color >> 8) & 0xFF
289+
blue = color & 0xFF
290+
for y in range(self.height):
291+
for x in range(self.width):
292+
addrs = self.pixel_addrs(x, y)
293+
self[addrs[self.r_offset]] = red
294+
self[addrs[self.g_offset]] = green
295+
self[addrs[self.b_offset]] = blue
296+
297+
def pixel(self, x, y, color=None):
298+
"""
299+
Set or retrieve RGB color of pixel at position (X,Y).
300+
301+
:param x: Horizontal pixel position.
302+
:param y: Vertical pixel position.
303+
:param color: If setting, a packed 24-bit color value (0xRRGGBB).
304+
If getting, either None or leave off this argument.
305+
:returns: If setting, returns None. If getting, returns a packed
306+
24-bit color value (0xRRGGBB).
307+
"""
308+
309+
if 0 <= x < self.width and 0 <= y < self.height: # Clip
310+
addrs = self.pixel_addrs(x, y) # LED indices
311+
# print(addrs)
312+
if color is not None:
313+
self[addrs[self.r_offset]] = (color >> 16) & 0xFF
314+
self[addrs[self.g_offset]] = (color >> 8) & 0xFF
315+
self[addrs[self.b_offset]] = color & 0xFF
316+
else: # Return current pixel color if unspecified
317+
return (
318+
(self[addrs[self.r_offset]] << 16)
319+
| (self[addrs[self.g_offset]] << 8)
320+
| self[addrs[self.b_offset]]
321+
)
322+
return None
323+
324+
def image(self, img):
325+
"""Copy an in-memory image to the LED matrix. Image should be in
326+
24-bit format (e.g. "RGB888") and dimensions should match matrix,
327+
this isn't super robust yet or anything.
328+
329+
:param img: Source image -- either a FrameBuffer object if running
330+
CircuitPython, or PIL image if running CPython w/Python
331+
Imaging Lib.
332+
"""
333+
if implementation.name == "circuitpython":
334+
for y in range(self.height):
335+
for x in range(self.width):
336+
self.pixel(x, y, img.pixel(x, y))
337+
else:
338+
if img.mode != "RGB":
339+
raise ValueError("Image must be in mode RGB.")
340+
if img.size[0] != self.width or img.size[1] != self.height:
341+
raise ValueError(
342+
"Image must be same dimensions as display ({0}x{1}).".format(
343+
self.width, self.height
344+
)
345+
)
346+
347+
# Iterate X/Y through all image pixels
348+
pixels = img.load() # Grab all pixels, faster than getpixel on each
349+
for y in range(self.height):
350+
for x in range(self.width):
351+
self.pixel(x, y, pixels[(x, y)])

0 commit comments

Comments
 (0)