#!/usr/bin/env python
# -*- coding: utf-8 -*-
import time
from max7219.font import DEFAULT_FONT
from max7219.rotate8x8 import rotate
[docs]class constants(object):
MAX7219_REG_NOOP = 0x0
MAX7219_REG_DIGIT0 = 0x1
MAX7219_REG_DIGIT1 = 0x2
MAX7219_REG_DIGIT2 = 0x3
MAX7219_REG_DIGIT3 = 0x4
MAX7219_REG_DIGIT4 = 0x5
MAX7219_REG_DIGIT5 = 0x6
MAX7219_REG_DIGIT6 = 0x7
MAX7219_REG_DIGIT7 = 0x8
MAX7219_REG_DECODEMODE = 0x9
MAX7219_REG_INTENSITY = 0xA
MAX7219_REG_SCANLIMIT = 0xB
MAX7219_REG_SHUTDOWN = 0xC
MAX7219_REG_DISPLAYTEST = 0xF
[docs]class device(object):
"""
Base class for handling multiple cascaded MAX7219 devices.
Callers should generally pick either the :py:class:`sevensegment` or
:py:class:`matrix` subclasses instead depending on which application
is required.
A buffer is maintained which holds the bytes that will be cascaded
every time :py:func:`flush` is called.
"""
NUM_DIGITS = 8
def __init__(self, cascaded=1, spi_bus=0, spi_device=0, vertical=False):
"""
Constructor: `cascaded` should be the number of daisy-chained MAX7219
devices that are connected. `vertical` should be set to True if
the text should start from the header instead perpendicularly.
"""
import spidev
assert cascaded > 0, "Must have at least one device!"
self._cascaded = cascaded
self._buffer = [0] * self.NUM_DIGITS * self._cascaded
self._spi = spidev.SpiDev()
self._spi.open(spi_bus, spi_device)
self._vertical = vertical
self.command(constants.MAX7219_REG_SCANLIMIT, 7) # show all 8 digits
self.command(constants.MAX7219_REG_DECODEMODE, 0) # use matrix (not digits)
self.command(constants.MAX7219_REG_DISPLAYTEST, 0) # no display test
self.command(constants.MAX7219_REG_SHUTDOWN, 1) # not shutdown mode
self.brightness(7) # intensity: range: 0..15
self.clear()
[docs] def command(self, register, data):
"""
Sends a specific register some data, replicated for all cascaded
devices
"""
assert constants.MAX7219_REG_DECODEMODE <= register <= constants.MAX7219_REG_DISPLAYTEST
self._write([register, data] * self._cascaded)
def _write(self, data):
"""
Send the bytes (which should comprise of alternating command,
data values) over the SPI device.
"""
self._spi.xfer2(list(data))
def _values(self, position, buf):
"""
A generator which yields the digit/column position and the data
value from that position for each of the cascaded devices.
"""
for deviceId in range(self._cascaded):
yield position + constants.MAX7219_REG_DIGIT0
yield buf[(deviceId * self.NUM_DIGITS) + position]
[docs] def clear(self, deviceId=None):
"""
Clears the buffer the given deviceId if specified (else clears all
devices), and flushes.
"""
assert not deviceId or 0 <= deviceId < self._cascaded, "Invalid deviceId: {0}".format(deviceId)
if deviceId is None:
start = 0
end = self._cascaded
else:
start = deviceId
end = deviceId + 1
for deviceId in range(start, end):
for position in range(self.NUM_DIGITS):
self.set_byte(deviceId,
position + constants.MAX7219_REG_DIGIT0,
0, redraw=False)
self.flush()
def _preprocess_buffer(self, buf):
"""
Overload in subclass to provide custom behaviour: see
matrix implementation for example.
"""
return buf
[docs] def flush(self):
"""
For each digit/column, cascade out the contents of the buffer
cells to the SPI device.
"""
# Allow subclasses to pre-process the buffer: they shouldn't
# alter it, so make a copy first.
buf = self._preprocess_buffer(list(self._buffer))
assert len(buf) == len(self._buffer), "Preprocessed buffer is wrong size"
if self._vertical:
tmp_buf = []
for x in range(0, self._cascaded):
tmp_buf += rotate(buf[x*8:x*8+8])
buf = tmp_buf
for posn in range(self.NUM_DIGITS):
self._write(self._values(posn, buf))
[docs] def brightness(self, intensity):
"""
Sets the brightness level of all cascaded devices to the same
intensity level, ranging from 0..15. Note that setting the brightness
to a high level will draw more current, and may cause intermittent
issues / crashes if the USB power source is insufficient.
"""
assert 0 <= intensity < 16, "Invalid brightness: {0}".format(intensity)
self.command(constants.MAX7219_REG_INTENSITY, intensity)
[docs] def set_byte(self, deviceId, position, value, redraw=True):
"""
Low level mechanism to set a byte value in the buffer array. If redraw
is not suppled, or set to True, will force a redraw of _all_ buffer
items: If you are calling this method rapidly/frequently (e.g in a
loop), it would be more efficient to set to False, and when done,
call :py:func:`flush`.
Prefer to use the higher-level method calls in the subclasses below.
"""
assert 0 <= deviceId < self._cascaded, "Invalid deviceId: {0}".format(deviceId)
assert constants.MAX7219_REG_DIGIT0 <= position <= constants.MAX7219_REG_DIGIT7, "Invalid digit/column: {0}".format(position)
assert 0 <= value < 256, 'Value {0} outside range 0..255'.format(value)
offset = (deviceId * self.NUM_DIGITS) + position - constants.MAX7219_REG_DIGIT0
self._buffer[offset] = value
if redraw:
self.flush()
[docs] def rotate_left(self, redraw=True):
"""
Scrolls the buffer one column to the left. The data that scrolls off
the left side re-appears at the right-most position. If redraw
is not suppled, or left set to True, will force a redraw of _all_ buffer
items
"""
t = self._buffer[-1]
for i in range((self.NUM_DIGITS * self._cascaded) - 1, 0, -1):
self._buffer[i] = self._buffer[i - 1]
self._buffer[0] = t
if redraw:
self.flush()
[docs] def rotate_right(self, redraw=True):
"""
Scrolls the buffer one column to the right. The data that scrolls off
the right side re-appears at the left-most position. If redraw
is not suppled, or left set to True, will force a redraw of _all_ buffer
items
"""
t = self._buffer[0]
for i in range(0, (self.NUM_DIGITS * self._cascaded) - 1, 1):
self._buffer[i] = self._buffer[i + 1]
self._buffer[-1] = t
if redraw:
self.flush()
[docs]class sevensegment(device):
"""
Implementation of MAX7219 devices cascaded with a series of seven-segment
LEDs. It provides a convenient method to write a number to a given device
in octal, decimal or hex, flushed left/right with zero padding. Base 10
numbers can be either integers or floating point (with the number of
decimal points configurable).
"""
_UNDEFINED = 0x08
_RADIX = {8: 'o', 10: 'f', 16: 'x'}
# Some letters cannot be represented by 7 segments, so dictionay lookup
# will default to _UNDEFINED (an underscore) instead.
_DIGITS = {
' ': 0x00,
'-': 0x01,
'_': 0x08,
'0': 0x7e,
'1': 0x30,
'2': 0x6d,
'3': 0x79,
'4': 0x33,
'5': 0x5b,
'6': 0x5f,
'7': 0x70,
'8': 0x7f,
'9': 0x7b,
'a': 0x7d,
'b': 0x1f,
'c': 0x0d,
'd': 0x3d,
'e': 0x6f,
'f': 0x47,
'g': 0x7b,
'h': 0x17,
'i': 0x10,
'j': 0x18,
# 'k': cant represent
'l': 0x06,
# 'm': cant represent
'n': 0x15,
'o': 0x1d,
'p': 0x67,
'q': 0x73,
'r': 0x05,
's': 0x5b,
't': 0x0f,
'u': 0x1c,
'v': 0x1c,
# 'w': cant represent
# 'x': cant represent
'y': 0x3b,
'z': 0x6d,
'A': 0x77,
'B': 0x7f,
'C': 0x4e,
'D': 0x7e,
'E': 0x4f,
'F': 0x47,
'G': 0x5e,
'H': 0x37,
'I': 0x30,
'J': 0x38,
# 'K': cant represent
'L': 0x0e,
# 'M': cant represent
'N': 0x76,
'O': 0x7e,
'P': 0x67,
'Q': 0x73,
'R': 0x46,
'S': 0x5b,
'T': 0x0f,
'U': 0x3e,
'V': 0x3e,
# 'W': cant represent
# 'X': cant represent
'Y': 0x3b,
'Z': 0x6d,
',': 0x80,
'.': 0x80
}
[docs] def letter(self, deviceId, position, char, dot=False, redraw=True):
"""
Looks up the most appropriate character representation for char
from the digits table, and writes that bitmap value into the buffer
at the given deviceId / position.
"""
assert dot in [0, 1, False, True]
value = self._DIGITS.get(str(char), self._UNDEFINED) | (dot << 7)
self.set_byte(deviceId, position, value, redraw)
[docs] def write_number(self, deviceId, value, base=10, decimalPlaces=0,
zeroPad=False, leftJustify=False):
"""
Formats the value according to the parameters supplied, and displays
on the specified device. If the formatted number is larger than
8 digits, then an OverflowError is raised.
"""
assert 0 <= deviceId < self._cascaded, "Invalid deviceId: {0}".format(deviceId)
assert base in self._RADIX, "Invalid base: {0}".format(base)
# Magic up a printf format string
size = self.NUM_DIGITS
formatStr = '%'
if zeroPad:
formatStr += '0'
if decimalPlaces > 0:
size += 1
if leftJustify:
size *= -1
formatStr = '{fmt}{size}.{dp}{type}'.format(
fmt=formatStr, size=size, dp=decimalPlaces,
type=self._RADIX[base])
position = constants.MAX7219_REG_DIGIT7
strValue = formatStr % value
# Go through each digit in the formatted string,
# updating the buffer accordingly
for char in strValue:
if position < constants.MAX7219_REG_DIGIT0:
self.clear(deviceId)
raise OverflowError('{0} too large for display'.format(strValue))
if char == '.':
continue
dp = (decimalPlaces > 0 and position == decimalPlaces + 1)
self.letter(deviceId, position, char, dot=dp, redraw=False)
position -= 1
self.flush()
[docs] def write_text(self, deviceId, text):
"""
Outputs the text (as near as possible) on the specific device. If
text is larger than 8 characters, then an OverflowError is raised.
"""
assert 0 <= deviceId < self._cascaded, "Invalid deviceId: {0}".format(deviceId)
if len(text) > 8:
raise OverflowError('{0} too large for display'.format(text))
for pos, char in enumerate(text.ljust(8)[::-1]):
self.letter(deviceId, constants.MAX7219_REG_DIGIT0 + pos, char, redraw=False)
self.flush()
[docs] def show_message(self, text, delay=0.4):
"""
Transitions the text message across the devices from left-to-right
"""
# Add some spaces on (same number as cascaded devices) so that the
# message scrolls off to the left completely.
text += ' ' * self._cascaded * 8
for value in text:
time.sleep(delay)
self.scroll_right(redraw=False)
self._buffer[0] = self._DIGITS.get(value, self._UNDEFINED)
self.flush()
[docs]class matrix(device):
"""
Implementation of MAX7219 devices cascaded with a series of 8x8 LED
matrix devices. It provides a convenient methods to write letters
to specific devices, to scroll a large message from left-to-right, or
to set specific pixels. It is assumed the matrices are linearly aligned.
"""
_invert = 0
_orientation = 0
[docs] def letter(self, deviceId, asciiCode, font=None, redraw=True):
"""
Writes the ASCII letter code to the given device in the specified font.
"""
assert 0 <= asciiCode < 256
if not font:
font = DEFAULT_FONT
col = constants.MAX7219_REG_DIGIT0
for value in font[asciiCode]:
if col > constants.MAX7219_REG_DIGIT7:
self.clear(deviceId)
raise OverflowError('Font for \'{0}\' too large for display'.format(asciiCode))
self.set_byte(deviceId, col, value, redraw=False)
col += 1
if redraw:
self.flush()
[docs] def show_message(self, text, font=None, delay=0.05, always_scroll=False):
"""
Shows a message on the device. If it's longer then the total width
(or always_scroll=True), it transitions the text message across the
devices from right-to-left.
"""
if not font:
font = DEFAULT_FONT
display_length = self.NUM_DIGITS * self._cascaded
src = [c for ascii_code in text for c in font[ord(ascii_code)]]
scroll = always_scroll or len(src) > display_length
if scroll:
# Add some spaces on (same number as cascaded devices) so that the
# message scrolls off to the left completely.
src += [c for ascii_code in ' ' * self._cascaded
for c in font[ord(ascii_code)]]
else:
# How much margin we need on the left so it's centered
margin = int((display_length - len(src))/2)
# Reset the buffer so no traces of the previous message are left
self._buffer = [0] * display_length
for pos, value in enumerate(src):
if scroll:
time.sleep(delay)
self.scroll_left(redraw=False)
self._buffer[-1] = value
self.flush()
else:
self._buffer[margin+pos] = value
if not scroll:
self.flush()
[docs] def pixel(self, x, y, value, redraw=True):
"""
Sets (value = 1) or clears (value = 0) the pixel at the given
co-ordinate. It may be more efficient to batch multiple pixel
operations together with redraw=False, and then call
:py:func:`flush` to redraw just once.
"""
assert 0 <= x < len(self._buffer)
assert 0 <= y < self.NUM_DIGITS
if value:
self._buffer[x] |= (1 << y)
else:
self._buffer[x] &= ~(1 << y)
if redraw:
self.flush()
def _rotate(self, buf):
"""
Rotates tiles in the buffer by the given orientation
"""
result = []
for i in range(0, self._cascaded * self.NUM_DIGITS, self.NUM_DIGITS):
tile = buf[i:i + self.NUM_DIGITS]
for _ in range(self._orientation // 90):
tile = rotate(tile)
result += tile
return result
def _preprocess_buffer(self, buf):
"""
Inverts and/or orientates the buffer before flushing according to
user set parameters
"""
if self._invert:
buf = [~x & 0xff for x in buf]
if self._orientation:
buf = self._rotate(buf)
return super(matrix, self)._preprocess_buffer(buf)
[docs] def invert(self, value, redraw=True):
"""
Sets whether the display should be inverted or not when displaying
letters.
"""
assert value in [0, 1, False, True]
self._invert = value
if redraw:
self.flush()
[docs] def orientation(self, angle, redraw=True):
"""
Sets the orientation (angle should be 0, 90, 180 or 270) at which
the characters are displayed.
"""
assert angle in [0, 90, 180, 270]
self._orientation = angle
if redraw:
self.flush()