diff --git a/CHANGES.rst b/CHANGES.rst index 261c185..f342657 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,7 +4,10 @@ ChangeLog +------------+------------------------------------------------------------------------+------------+ | Version | Description | Date | +============+========================================================================+============+ -| **0.9.0** | * Add support for APA102 RGB neopixels | | +| **0.10.0** | * **BREAKING CHANGE:** Move sevensegment class to | 2017/04/14 | +| | ``luma.core.virtual`` package | | ++------------+------------------------------------------------------------------------+------------+ +| **0.9.0** | * Add support for APA102 RGB neopixels | 2017/03/30 | +------------+------------------------------------------------------------------------+------------+ | **0.8.0** | * Change MAX7219's block_orientation to support ±90° angle correction | 2017/03/19 | | | * Deprecate "vertical" and "horizontal" block_orientation | | diff --git a/doc/python-usage.rst b/doc/python-usage.rst index c7651c4..560c665 100644 --- a/doc/python-usage.rst +++ b/doc/python-usage.rst @@ -220,7 +220,7 @@ representation is corrected to reverse the 90° phase shift. 7-Segment LED Displays ^^^^^^^^^^^^^^^^^^^^^^ -For the 7-segment device, initialize the :py:class:`luma.led_matrix.virtual.sevensegment` +For the 7-segment device, initialize the :py:class:`luma.core.virtual.sevensegment` class, and wrap it around a previously created :py:class:`~luma.led_matrix.device.max7219` device: @@ -228,14 +228,14 @@ device: from luma.core.serial import spi, noop from luma.core.render import canvas + from luma.core.virtual import sevensegment from luma.led_matrix.device import max7219 - from luma.led_matrix.virtual import sevensegment serial = spi(port=0, device=0, gpio=noop()) device = max7219(serial, cascaded=2) seg = sevensegment(device) -The **seg** instance now has a :py:attr:`~luma.led_matrix.virtual.sevensegment.text` +The **seg** instance now has a :py:attr:`~luma.core.virtual.sevensegment.text` property which may be assigned, and when it does will update all digits according to the limited alphabet the 7-segment displays support. For example, assuming there are 2 cascaded modules, we have 16 character available, and so diff --git a/examples/7segment_scroll.py b/examples/7segment_scroll.py index d36ed95..86f19cf 100755 --- a/examples/7segment_scroll.py +++ b/examples/7segment_scroll.py @@ -9,10 +9,9 @@ """ import time -from luma.core.emulator import pygame -from luma.core.virtual import viewport -from luma.led_matrix.virtual import sevensegment - +from luma.emulator.device import pygame +from luma.core.virtual import viewport, sevensegment +from luma.led_matrix.segment_mapper import dot_muncher blurb = """ Have you ever been to American wedding? @@ -59,7 +58,7 @@ def main(): device = pygame(width=24, height=8, transform="sevensegment", scale=1) virtual = viewport(device, width=1024, height=8) - seg = sevensegment(virtual) + seg = sevensegment(virtual, segment_mapper=dot_muncher) for line in blurb.split("\n"): seg.text = (" " * device.width) + line diff --git a/examples/sevensegment_demo.py b/examples/sevensegment_demo.py index 6a9c7be..da9fd50 100755 --- a/examples/sevensegment_demo.py +++ b/examples/sevensegment_demo.py @@ -11,9 +11,8 @@ from datetime import datetime from luma.led_matrix.device import max7219 -from luma.led_matrix.virtual import sevensegment from luma.core.serial import spi, noop -from luma.core.virtual import viewport +from luma.core.virtual import viewport, sevensegment def date(seg): diff --git a/luma/led_matrix/device.py b/luma/led_matrix/device.py index b8eb363..ce10df6 100644 --- a/luma/led_matrix/device.py +++ b/luma/led_matrix/device.py @@ -33,6 +33,7 @@ from luma.core.util import deprecation import luma.core.error import luma.led_matrix.const +from luma.led_matrix.segment_mapper import dot_muncher __all__ = ["max7219", "ws2812", "neopixel", "apa102"] @@ -55,6 +56,7 @@ def __init__(self, serial_interface=None, width=8, height=8, cascaded=None, rota height = 8 self.capabilities(width, height, rotate) + self.segment_mapper = dot_muncher if width <= 0 or width % 8 != 0 or height <= 0 or height % 8 != 0: raise luma.core.error.DeviceDisplayModeError( diff --git a/luma/led_matrix/helpers.py b/luma/led_matrix/helpers.py deleted file mode 100644 index 2d024c8..0000000 --- a/luma/led_matrix/helpers.py +++ /dev/null @@ -1,83 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2017 Richard Hull and contributors -# See LICENSE.rst for details. - - -class mutable_string(object): - - def __init__(self, value): - assert value.__class__ is str - self.target = value - - def __getattr__(self, attr): - return self.target.__getattribute__(attr) - - def __getitem__(self, key): - return self.target[key] - - def __setitem__(self, key, value): - assert value.__class__ is str - tmp = list(self.target) - tmp[key] = value - self.target = "".join(tmp) - - def __delitem__(self, key): - tmp = list(self.target) - del tmp[key] - self.target = "".join(tmp) - - def __len__(self): - return len(self.target) - - def __iter__(self): - return iter(self.target) - - def __str__(self): - return str(self.target) - - def __repr__(self): - return repr(self.target) - - def __eq__(self, other): - return str(self.target) == str(other) - - def __hash__(self): - return hash(self.target) - - -class observable(object): - """ - Wraps any container object such that on inserting, updating or deleting, - an observer is notified with a payload of the target. All other special name - methods are passed through parameters unhindered. - """ - def __init__(self, target, observer): - self.target = target - self.observer = observer - self.observer(self.target) - - def __getattr__(self, attr): - return self.target.__getattribute__(attr) - - def __getitem__(self, key): - return self.target.__getitem__(key) - - def __setitem__(self, key, value): - self.target.__setitem__(key, value) - self.observer(self.target) - - def __delitem__(self, key): - self.target.__delitem__(key) - self.observer(self.target) - - def __len__(self): - return self.target.__len__() - - def __iter__(self): - return self.target.__iter__() - - def __str__(self): - return self.target.__str__() - - def __repr__(self): - return self.target.__repr__() diff --git a/luma/led_matrix/virtual.py b/luma/led_matrix/virtual.py deleted file mode 100644 index 775447e..0000000 --- a/luma/led_matrix/virtual.py +++ /dev/null @@ -1,92 +0,0 @@ -# -*- 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.render import canvas -from luma.led_matrix.segment_mapper import dot_muncher -from luma.led_matrix.helpers import mutable_string, observable - - -class sevensegment(object): - """ - Abstraction that wraps a MAX7219 device, this class provides a ``text`` - property which can be used to set and get a value, which is propagated - onto the underlying device. - - :param device: A MAX7219 device instance - :param undefined: The default character to substitute when an unrenderable - character is supplied to the text property. - :type undefined: char - :param mapper: A function that maps strings into the correct - representation for the 7-segment physical layout. By default, a "dot" - muncher implementation is used which places dots inline with the - preceeding character. - """ - def __init__(self, device, undefined="_", mapper=dot_muncher): - self.device = device - self.undefined = undefined - self.segment_mapper = mapper - self._bufsize = device.width * device.height // 8 - self.text = "" - - @property - def text(self): - """ - Returns the current state of the text buffer. This may not reflect - accurately what is displayed on the seven-segment device, as certain - alpha-numerics and most punctuation cannot be rendered on the limited - display - """ - return self._text_buffer - - @text.setter - def text(self, value): - """ - Updates the seven-segment display with the given value. If there is not - enough space to show the full text, an ``OverflowException`` is raised. - - :param value: The value to render onto the device. Any characters which - cannot be rendered will be converted into the ``undefined`` - character supplied in the constructor. - :type value: string - """ - self._text_buffer = observable(mutable_string(value), observer=self._flush) - - def _flush(self, buf): - data = bytearray(self.segment_mapper(buf, notfound=self.undefined)).ljust(self._bufsize, b'\0') - - if len(data) > self._bufsize: - raise OverflowError("Device's capabilities insufficent for value '{0}'".format(buf)) - - with canvas(self.device) as draw: - for x, byte in enumerate(reversed(data)): - for y in range(8): - if byte & 0x01: - draw.point((x, y), fill="white") - byte >>= 1 diff --git a/setup.py b/setup.py index c210a4b..4be4141 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ def read_file(fname): download_url="https://github.com/rm-hull/luma.led_matrix/tarball/" + version, namespace_packages=["luma"], packages=["luma.led_matrix"], - install_requires=["luma.core>=0.7.4", "ws2812"], + install_requires=["luma.core>=0.8.1", "ws2812"], setup_requires=pytest_runner, tests_require=["mock", "pytest", "pytest-cov"], extras_require={ diff --git a/tests/reference/golden_ratio.png b/tests/reference/golden_ratio.png deleted file mode 100644 index 35e3eae..0000000 Binary files a/tests/reference/golden_ratio.png and /dev/null differ diff --git a/tests/test_observable.py b/tests/test_observable.py deleted file mode 100644 index 040a031..0000000 --- a/tests/test_observable.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# Copyright (c) 2014-17 Richard Hull and contributors -# See LICENSE.rst for details. - - -from luma.led_matrix.helpers import observable, mutable_string - - -class test_bell(object): - - called = 0 - - def ding(self, *args): - self.called += 1 - - -def test_length(): - bell = test_bell() - buf = observable(bytearray("hello", "utf-8"), bell.ding) - assert len(buf) == 5 - assert bell.called == 1 - - -def test_iteration(): - bell = test_bell() - buf = observable(bytearray("hello", "utf-8"), bell.ding) - assert list(iter(buf)) == [ord(ch) for ch in "hello"] - assert bell.called == 1 - - -def test_getattribute(): - bell = test_bell() - buf = observable(bytearray("hello", "utf-8"), bell.ding) - assert list(iter(buf.decode("utf-8"))) == list("hello") - assert bell.called == 1 - - -def test_getitem(): - bell = test_bell() - buf = observable(mutable_string("hello"), bell.ding) - assert buf[2] == "l" - assert bell.called == 1 - - -def test_setitem(): - bell = test_bell() - buf = observable(mutable_string("hello"), bell.ding) - buf[0] = "y" - assert str(buf) == "yello" - assert bell.called == 2 - - -def test_setslice(): - bell = test_bell() - buf = observable(mutable_string("hello"), bell.ding) - buf[1:4] = "ipp" - assert str(buf) == "hippo" - assert bell.called == 2 - - -def test_delitem(): - bell = test_bell() - buf = observable(mutable_string("hello"), bell.ding) - del buf[4] - assert str(buf) == "hell" - assert bell.called == 2 - - -def test_getslice(): - bell = test_bell() - buf = observable(mutable_string("hello"), bell.ding) - assert buf[2:4] == "ll" - assert bell.called == 1 - - -def test_repr(): - bell = test_bell() - buf = observable(bytearray("hello", "utf-8"), bell.ding) - assert repr(buf) == "bytearray(b'hello')" - assert bell.called == 1 diff --git a/tests/test_segment_mapper.py b/tests/test_segment_mapper.py index 65ba5aa..a3e4a31 100644 --- a/tests/test_segment_mapper.py +++ b/tests/test_segment_mapper.py @@ -4,7 +4,7 @@ # See LICENSE.rst for details. -from luma.led_matrix.helpers import mutable_string +from luma.core.util import mutable_string from luma.led_matrix.segment_mapper import dot_muncher, regular diff --git a/tests/test_sevensegment.py b/tests/test_sevensegment.py deleted file mode 100644 index 86a883b..0000000 --- a/tests/test_sevensegment.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# Copyright (c) 2014-17 Richard Hull and contributors -# See LICENSE.rst for details. - -import pytest - -from PIL import Image, ImageChops -from luma.led_matrix.virtual import sevensegment -from luma.core.device import dummy - -from helpers import get_reference_image - - -def test_init(): - device = dummy(width=24, height=8, mode="1") - sevensegment(device) - assert device.image == Image.new("1", (24, 8)) - - -def test_overflow(): - device = dummy(width=24, height=8, mode="1") - seg = sevensegment(device) - with pytest.raises(OverflowError) as ex: - seg.text = "This is too big to fit in 3x8 seven-segment displays" - assert str(ex.value) == "Device's capabilities insufficent for value 'This is too big to fit in 3x8 seven-segment displays'" - - -def test_setter_getter(): - img_path = get_reference_image('golden_ratio.png') - - with open(img_path, 'rb') as img: - reference = Image.open(img) - device = dummy(width=24, height=8) - seg = sevensegment(device) - seg.text = "1.61803398875" - assert str(seg.text) == "1.61803398875" - - bbox = ImageChops.difference(reference, device.image).getbbox() - assert bbox is None