# -*- coding: utf-8 -*-
# Copyright (c) 2017 Richard Hull and contributors
# See LICENSE.rst for details.
# Example usage:
#
# from luma.core.serial import spi, noop
# from luma.core.render import canvas
# from luma.led_matrix.device import max7219
#
# serial = spi(port=0, device=0, gpio=noop())
# device = max7219(serial, width=8, height=8)
#
# with canvas(device) as draw:
# draw.rectangle(device.bounding_box, outline="white", fill="black")
#
# As soon as the with-block scope level is complete, the graphics primitives
# will be flushed to the device.
#
# Creating a new canvas is effectively 'carte blanche': If you want to retain
# an existing canvas, then make a reference like:
#
# c = canvas(device)
# for X in ...:
# with c as draw:
# draw.rectangle(...)
#
# As before, as soon as the with block completes, the canvas buffer is flushed
# to the device
from luma.core.serial import noop
from luma.core.device import device
import luma.core.error
import luma.led_matrix.const
__all__ = ["max7219", "neopixel"]
[docs]class max7219(device):
"""
Encapsulates the serial interface to a series of 8x8 LED matrixes
daisychained together with MAX7219 chips. On creation, an initialization
sequence is pumped to the display to properly configure it. Further control
commands can then be called to affect the brightness and other settings.
"""
def __init__(self, serial_interface=None, width=8, height=8, cascaded=None, rotate=0,
block_orientation="horizontal", **kwargs):
super(max7219, self).__init__(luma.led_matrix.const.max7219, serial_interface)
# Derive (override) the width and height if a cascaded param supplied
if cascaded is not None:
width = cascaded * 8
height = 8
self.capabilities(width, height, rotate)
if width <= 0 or width % 8 != 0 or height <= 0 or height % 8 != 0:
raise luma.core.error.DeviceDisplayModeError(
"Unsupported display mode: {0} x {1}".format(width, height))
self.cascaded = cascaded or (width * height) // 64
assert(block_orientation in ["horizontal", "vertical"])
self._block_orientation = block_orientation
self._offsets = [(y * self._w) + x
for y in range(self._h - 8, -8, -8)
for x in range(self._w - 8, -8, -8)]
self._rows = list(range(8))
self.data([self._const.SCANLIMIT, 7] * self.cascaded)
self.data([self._const.DECODEMODE, 0] * self.cascaded)
self.data([self._const.DISPLAYTEST, 0] * self.cascaded)
self.contrast(0x70)
self.clear()
self.show()
[docs] def preprocess(self, image):
"""
Performs the inherited behviour (if any), and if the LED matrix is
declared to being a common row cathode, each 8x8 block of pixels
is rotated 90° clockwise.
"""
image = super(max7219, self).preprocess(image)
if self._block_orientation == "vertical":
image = image.copy()
for y in range(0, self._h, 8):
for x in range(0, self._w, 8):
box = (x, y, x + 8, y + 8)
rotated_block = image.crop(box).rotate(-90)
image.paste(rotated_block, box)
return image
[docs] def display(self, image):
"""
Takes a 1-bit :py:mod:`PIL.Image` and dumps it to the LED matrix display
via the MAX7219 serializers.
"""
assert(image.mode == self.mode)
assert(image.size == self.size)
image = self.preprocess(image)
i = 0
d0 = self._const.DIGIT_0
step = 2 * self.cascaded
offsets = self._offsets
rows = self._rows
buf = bytearray(8 * step)
pix = list(image.getdata())
for digit in range(8):
for daisychained_device in offsets:
byte = 0
idx = daisychained_device + digit
for y in rows:
if pix[idx] > 0:
byte |= 1 << y
idx += self._w
buf[i] = digit + d0
buf[i + 1] = byte
i += 2
buf = list(buf)
for i in range(0, len(buf), step):
self.data(buf[i:i + step])
[docs] def contrast(self, value):
"""
Sets the LED intensity to the desired level, in the range 0-255.
:param level: Desired contrast level in the range of 0-255.
:type level: int
"""
assert(0 <= value <= 255)
self.data([self._const.INTENSITY, value >> 4] * self.cascaded)
[docs] def show(self):
"""
Switches the display mode OFF, putting the device in low-power
sleep mode.
"""
self.data([self._const.SHUTDOWN, 1] * self.cascaded)
[docs] def hide(self):
"""
Sets the display mode ON, waking the device out of a prior
low-power sleep mode.
"""
self.data([self._const.SHUTDOWN, 0] * self.cascaded)
[docs]class neopixel(device):
"""
Encapsulates the serial interface to a series of RGB neopixels
daisy-chained together with WS281x chips. On creation, the array is
initialized with the correct number of cascaded devices. Further control
commands can then be called to affect the brightness and other settings.
:param dma_interface: The WS2812 interface to write to (usually omit this
parameter and it will default to the correct value - it is only needed
for testing whereby a mock implementation is supplied)
:param width: The number of pixels laid out horizontally
:type width: int
:param height: The number of pixels laid out vertically
:type width: int
:param cascaded: The number of pixels in a single strip - if supplied, this
will override ``width`` and ``height``.
:type width: int
:param rotate: Whether the device dimenstions should be rotated in-situ:
A value of: 0=0°, 1=90°, 2=180°, 3=270°. If not supplied, zero is
assumed.
:type rotate: int
:param mapping: An (optional) array of integer values that translate the
pixel to physical offsets. If supplied, should be the same size as
``width * height``
:type mapping: int[]
"""
def __init__(self, dma_interface=None, width=8, height=4, cascaded=None,
rotate=0, mapping=None, **kwargs):
super(neopixel, self).__init__(const=None, serial_interface=noop)
# Derive (override) the width and height if a cascaded param supplied
if cascaded is not None:
width = cascaded
height = 1
self.cascaded = width * height
self.capabilities(width, height, rotate, mode="RGB")
self._mapping = list(mapping or range(self.cascaded))
assert(self.cascaded == len(self._mapping))
self._ws2812 = dma_interface or self.__ws2812__()
self._ws2812.init(width * height)
self.contrast(0x70)
self.clear()
self.show()
def __ws2812__(self):
import ws2812
return ws2812
[docs] def display(self, image):
"""
Takes a 24-bit RGB :py:mod:`PIL.Image` and dumps it to the daisy-chained
WS2812 neopixels.
"""
assert(image.mode == self.mode)
assert(image.size == self.size)
ws = self._ws2812
m = self._mapping
for idx, (r, g, b) in enumerate(image.getdata()):
ws.setPixelColor(m[idx], r, g, b)
ws.show()
[docs] def show(self):
"""
Not supported
"""
pass
[docs] def hide(self):
"""
Not supported
"""
pass
[docs] def contrast(self, value):
"""
Sets the LED intensity to the desired level, in the range 0-255.
:param level: Desired contrast level in the range of 0-255.
:type level: int
"""
assert(0 <= value <= 255)
ws = self._ws2812
ws.setBrightness(value / 255.0)
ws.show()
[docs] def cleanup(self):
"""
Attempt to reset the device & switching it off prior to exiting the
python process.
"""
super(neopixel, self).cleanup()
self._ws2812.terminate()
# 8x8 Unicorn HAT has a 'snake-like' layout, so this translation
# mapper linearizes that arrangement into a 'scan-like' layout.
UNICORN_HAT = [
7, 6, 5, 4, 3, 2, 1, 0,
8, 9, 10, 11, 12, 13, 14, 15,
23, 22, 21, 20, 19, 18, 17, 16,
24, 25, 26, 27, 28, 29, 30, 31,
39, 38, 37, 36, 35, 34, 33, 32,
40, 41, 42, 43, 44, 45, 46, 47,
55, 54, 53, 52, 51, 50, 49, 48,
56, 57, 58, 59, 60, 61, 62, 63
]