Skip to content

Commit

Permalink
Support neosegment displays (#112)
Browse files Browse the repository at this point in the history
* Add support for NeoSegments

* Add width checking

* Use device height

* Add docstring

* Add docs

* Updated docs

* Rename device

* Rework color setting

* Add tests

* Remove unnecessary segment_mapper assignment

* Fix tests

* Tidy

* Add neosegment to __all__

* Fix crash

* Update changelog

* Address PR review comments
  • Loading branch information
rm-hull committed Jul 21, 2017
1 parent 34e5716 commit eeff913
Show file tree
Hide file tree
Showing 9 changed files with 343 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ ChangeLog
| Version | Description | Date |
+============+========================================================================+============+
| *Upcoming* | * Alternative WS2812 low level implementation | |
| | * Add support for @msurguy's modular NeoSegments | |
+------------+------------------------------------------------------------------------+------------+
| **0.10.1** | * Add block_orientation=180 option | 2017/05/01 |
+------------+------------------------------------------------------------------------+------------+
Expand Down
13 changes: 13 additions & 0 deletions doc/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,19 @@ The DO pin should be connected to the DI pin on the next (daisy-chained)
neopixel, while the VCC and VSS are supplied in-parallel to all LED's.
WS2812b devices now are becoming more prevalent, and only have 4 pins.

NeoSegments
"""""""""""
@msurguy's NeoSegments should be connected as follows:

============ ====== ============= ========= ====================
Board Pin Name Remarks RPi Pin RPi Function
------------ ------ ------------- --------- --------------------
1 GND Ground 6 GND
2 DI Data In 12 GPIO 18 (PWM0)
3 VCC +5V Power 2 5V0
============ ====== ============= ========= ====================


Installing from PyPi
^^^^^^^^^^^^^^^^^^^^
Install the dependencies for library first with::
Expand Down
38 changes: 36 additions & 2 deletions doc/python-usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ As soon as the with scope is ended, the resultant image is automatically
flushed to the device's display memory and the :mod:`PIL.ImageDraw` object is
garbage collected.

.. note::
.. note::
The default Pillow font is too big for 8px high devices like the LED matrices
here, so the `luma.examples <https://github.com/rm-hull/luma.examples>`_ repo
inclues a small TTF pixel font called **pixelmix.ttf** (attribution:
Expand All @@ -62,7 +62,6 @@ garbage collected.

Scrolling / Virtual viewports
"""""""""""""""""""""""""""""

A single 8x8 LED matrix clearly hasn't got a lot of area for displaying useful
information. Obviously they can be daisy-chained together to provide a longer
line of text, but as this library extends `luma.core <https://github.com/rm-hull/luma.core>`_,
Expand Down Expand Up @@ -341,6 +340,41 @@ a translation mapping is required, as follows:
This should animate a green dot moving left-to-right down each line.

NeoSegments
"""""""""""
`@msurguy <https://twitter.com/msurguy?lang=en>`_ has `crowdsourced some WS2812 neopixels <https://www.crowdsupply.com/maksmakes/neosegment>`_
into a modular 3D-printed seven-segment unit. To program these devices:

.. code:: python
import time
from luma.led_matrix_device import neosegment
neoseg = neosegment(width=6)
# Defaults to "white" color initially
neoseg.text = "NEOSEG"
time.sleep(1)
# Set the first char ('N') to red
neoseg.color[0] = "red"
time.sleep(1)
# Set fourth and fifth chars ('S','E') accordingly
neoseg.color[3:5] = ["cyan", "blue"]
time.sleep(1)
# Set the entire string to green
neoseg.color = "green"
The :py:class:`~luma.led_matrix.device.neosegment` class extends :py:class:`~luma.core.virtual.sevensegment`,
so the same text assignment (Python slicing paradigms) can be used here as well -
see the earlier section for further details.

The underlying device is exposed as attribute :py:attr:`device`, so methods
such as :py:attr:`show`, :py:attr:`hide` and :py:attr:`contrast` are available.

Next-generation APA102 NeoPixels
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
APA102 RGB neopixels are easier to control that WS2812 devices - they are driven
Expand Down
4 changes: 2 additions & 2 deletions examples/neopixel_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,8 @@ def main():
time.sleep(1)

with canvas(device) as draw:
text(draw, (0, -1), text="A", fill="red", font=TINY_FONT)
text(draw, (4, -1), text="T", fill="green", font=TINY_FONT)
text(draw, (0, -1), txt="A", fill="red", font=TINY_FONT)
text(draw, (4, -1), txt="T", fill="green", font=TINY_FONT)

time.sleep(1)

Expand Down
94 changes: 94 additions & 0 deletions examples/neosegment_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2017 Richard Hull and contributors
# See LICENSE.rst for details.

import time
import random
import colorsys
from luma.led_matrix.device import neosegment
from neopixel_demo import gfx


def rainbow(n=1000, saturation=1, value=1):
"""
A generator that yields 'n' hues from the rainbow in the hex format #RRGGBB.
By default the saturation and value (from HSV) are both set to 1.
"""
for i in range(n):
hue = i / float(n)
color = [int(x * 255) for x in colorsys.hsv_to_rgb(hue, saturation, value)]
yield ("#%02x%02x%02x" % tuple(color)).upper()


def main():
neoseg = neosegment(width=6)
neoseg.text = "NEOSEG"
time.sleep(1)
neoseg.color[0] = "yellow"
time.sleep(1)
neoseg.color[3:5] = ["blue", "orange"]
time.sleep(1)
neoseg.color = "white"
time.sleep(1)

for _ in range(10):
neoseg.device.hide()
time.sleep(0.1)
neoseg.device.show()
time.sleep(0.1)

time.sleep(1)

for color in rainbow(200):
neoseg.color = color
time.sleep(0.01)

colors = list(rainbow(neoseg.device.width))
for _ in range(50):
random.shuffle(colors)
neoseg.color = colors
time.sleep(0.1)

neoseg.color = "white"
time.sleep(3)

for _ in range(3):
for intensity in range(16):
neoseg.device.contrast((15 - intensity) * 16)
time.sleep(0.1)

for intensity in range(16):
neoseg.device.contrast(intensity * 16)
time.sleep(0.1)

neoseg.text = ""
neoseg.device.contrast(0x80)
time.sleep(1)

neoseg.text = "rgb"
time.sleep(1)
neoseg.color[0] = "red"
time.sleep(1)
neoseg.color[1] = "green"
time.sleep(1)
neoseg.color[2] = "blue"
time.sleep(5)

for _ in range(3):
for intensity in range(16):
neoseg.device.contrast(intensity * 16)
time.sleep(0.1)

neoseg.text = ""
neoseg.device.contrast(0x80)
time.sleep(1)

gfx(neoseg.device)


if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
pass
98 changes: 93 additions & 5 deletions luma/led_matrix/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,17 @@
# As before, as soon as the with block completes, the canvas buffer is flushed
# to the device

from luma.core.interface.serial import noop
from luma.core.device import device
from luma.core.util import deprecation
import luma.core.error
import luma.led_matrix.const
from luma.led_matrix.segment_mapper import dot_muncher
from luma.core.interface.serial import noop
from luma.core.device import device
from luma.core.render import canvas
from luma.core.util import deprecation, observable
from luma.core.virtual import sevensegment
from luma.led_matrix.segment_mapper import dot_muncher, regular


__all__ = ["max7219", "ws2812", "neopixel", "apa102"]
__all__ = ["max7219", "ws2812", "neopixel", "neosegment", "apa102"]


class max7219(device):
Expand Down Expand Up @@ -393,3 +395,89 @@ def contrast(self, value):
self._brightness = value >> 4
if self._last_image is not None:
self.display(self._last_image)


class neosegment(sevensegment):
"""
Extends the :py:class:`~luma.core.virtual.sevensegment` class specifically
for @msurguy's modular NeoSegments. It uses the same underlying render
techniques as the base class, but provides additional functionality to be
able to adddress individual characters colors.
:param width: The number of 7-segment elements that are cascaded.
:type width: int
:param undefined: The default character to substitute when an unrenderable
character is supplied to the text property.
:type undefined: char
.. versionadded:: 0.11.0
"""
def __init__(self, width, undefined="_", **kwargs):
if width <= 0 or width % 2 == 1:
raise luma.core.error.DeviceDisplayModeError(
"Unsupported display mode: width={0}".format(width))

height = 7
mapping = [(i % width) * height + (i // width) for i in range(width * height)]
self.device = kwargs.get("device") or ws2812(width=width, height=height, mapping=mapping)
self.undefined = undefined
self._text_buffer = ""
self.color = "white"

@property
def color(self):
return self._colors

@color.setter
def color(self, value):
if not isinstance(value, list):
value = [value] * self.device.width

assert(len(value) == self.device.width)
self._colors = observable(value, observer=self._color_chg)

def _color_chg(self, color):
self._flush(self.text, color)

def _flush(self, text, color=None):
data = bytearray(self.segment_mapper(text, notfound=self.undefined)).ljust(self.device.width, b'\0')
color = color or self.color

if len(data) > self.device.width:
raise OverflowError(
"Device's capabilities insufficient for value '{0}'".format(text))

with canvas(self.device) as draw:
for x, byte in enumerate(data):
for y in range(self.device.height):
if byte & 0x01:
draw.point((x, y), fill=color[x])
byte >>= 1

def segment_mapper(self, text, notfound="_"):
try:
iterator = regular(text, notfound)
while True:
char = next(iterator)

# Convert from std MAX7219 segment mappings
a = char >> 6 & 0x01
b = char >> 5 & 0x01
c = char >> 4 & 0x01
d = char >> 3 & 0x01
e = char >> 2 & 0x01
f = char >> 1 & 0x01
g = char >> 0 & 0x01

# To NeoSegment positions
yield \
b << 6 | \
a << 5 | \
f << 4 | \
g << 3 | \
c << 2 | \
d << 1 | \
e << 0

except StopIteration:
pass
12 changes: 11 additions & 1 deletion tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from mock import call, Mock # noqa: F401

import pytest
from PIL import ImageChops

import luma.core.error

Expand All @@ -34,8 +35,17 @@ def assert_invalid_dimensions(deviceType, serial_interface, width, height):
assert "Unsupported display mode: {} x {}".format(width, height) in str(ex.value)


def get_reference_image(fname):
def get_reference_file(fname):
return os.path.abspath(os.path.join(
os.path.dirname(__file__),
'reference',
fname))


def get_reference_image(fname):
return get_reference_file(os.path.join('images', fname))


def assert_identical_image(reference, target):
bbox = ImageChops.difference(reference, target).getbbox()
assert bbox is None
Binary file added tests/reference/images/neosegment.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit eeff913

Please sign in to comment.