Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

New diff-to-previous framebuffer implementation #129

Merged
merged 4 commits into from
Nov 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ ChangeLog
+------------+---------------------------------------------------------------------+------------+
| Version | Description | Date |
+============+=====================================================================+============+
| *TBC* | * Improved performance for ST7739 and ILI9341 displays | |
+------------+---------------------------------------------------------------------+------------+
| **2.6.0** | * Drop support for Python 3.5, only 3.6 or newer is supported now | 2020/10/25 |
| | * Pin luma.core to 1.x.y line only, in anticipation of performance | |
| | improvements in upcoming major release | |
Expand Down
94 changes: 52 additions & 42 deletions luma/lcd/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from luma.core.lib import rpi_gpio
from luma.core.device import device, parallel_device
from luma.core.interface.serial import noop, pcf8574
from luma.core.framebuffer import diff_to_previous
import luma.core.error
import luma.core.framebuffer
import luma.lcd.const
Expand Down Expand Up @@ -366,7 +367,30 @@ def contrast(self, value):
self.command(0x81, value)


class st7735(backlit_device):
class __framebuffer_mixin(object):
"""
Helper class for initializing the framebuffer. Its only purpose is to
log a deprecation warning if a string framebuffer is specified.

.. note::
Specifying the framebuffer as a string will be removed at the next
major release, and hence this mixin will become redundant and will
also be removed at that point.
"""

def init_framebuffer(self, framebuffer):
if isinstance(framebuffer, str):
import warnings
warnings.warn(
"Specifying framebuffer as a string is now deprecated; Supply an instance of class full_frame() or diff_to_previous() instead",
DeprecationWarning
)
self.framebuffer = getattr(luma.core.framebuffer, framebuffer)()
else:
self.framebuffer = framebuffer


class st7735(backlit_device, __framebuffer_mixin):
"""
Serial interface to a 262K color (6-6-6 RGB) ST7735 LCD display.

Expand All @@ -385,9 +409,9 @@ class st7735(backlit_device):
no rotation, 1 is rotate 90掳 clockwise, 2 is 180掳 rotation and 3
represents 270掳 rotation.
:type rotate: int
:param framebuffer: Framebuffering strategy, currently values of
``diff_to_previous`` or ``full_frame`` are only supported.
:type framebuffer: str
:param framebuffer: Framebuffering strategy, currently instances of
``diff_to_previous()`` or ``full_frame()`` are only supported.
:type framebuffer: luma.core.framebuffer.framebuffer
:param bgr: Set to ``True`` if device pixels are BGR order (rather than RGB).
:type bgr: bool
:param inverse: Set to ``True`` if device pixels are inversed.
Expand All @@ -402,11 +426,11 @@ class st7735(backlit_device):
.. versionadded:: 0.3.0
"""
def __init__(self, serial_interface=None, width=160, height=128, rotate=0,
framebuffer="diff_to_previous", h_offset=0, v_offset=0,
framebuffer=diff_to_previous(num_segments=16), h_offset=0, v_offset=0,
bgr=False, inverse=False, **kwargs):
super(st7735, self).__init__(luma.lcd.const.st7735, serial_interface, **kwargs)
self.capabilities(width, height, rotate, mode="RGB")
self.framebuffer = getattr(luma.core.framebuffer, framebuffer)(self)
self.init_framebuffer(framebuffer)

if h_offset != 0 or v_offset != 0:
def offset(bbox):
Expand Down Expand Up @@ -466,26 +490,14 @@ def display(self, image):

image = self.preprocess(image)

if self.framebuffer.redraw_required(image):
left, top, right, bottom = self.apply_offsets(self.framebuffer.bounding_box)
width = right - left
height = bottom - top
for image, bounding_box in self.framebuffer.redraw(image):
left, top, right, bottom = self.apply_offsets(bounding_box)

self.command(0x2A, left >> 8, left & 0xFF, (right - 1) >> 8, (right - 1) & 0xFF) # Set column addr
self.command(0x2B, top >> 8, top & 0xFF, (bottom - 1) >> 8, (bottom - 1) & 0xFF) # Set row addr
self.command(0x2C) # Memory write

i = 0
buf = bytearray(width * height * 3)
for r, g, b in self.framebuffer.getdata():
if not(r == g == b == 0):
# 262K format
buf[i] = r
buf[i + 1] = g
buf[i + 2] = b
i += 3

self.data(list(buf))
self.data(list(image.tobytes()))

def contrast(self, level):
"""
Expand All @@ -507,7 +519,7 @@ def command(self, cmd, *args):
self._serial_interface.data(list(args))


class ili9341(backlit_device):
class ili9341(backlit_device, __framebuffer_mixin):
"""
Serial interface to a 262k color (6-6-6 RGB) ILI9341 LCD display.

Expand All @@ -526,9 +538,9 @@ class ili9341(backlit_device):
no rotation, 1 is rotate 90掳 clockwise, 2 is 180掳 rotation and 3
represents 270掳 rotation.
:type rotate: int
:param framebuffer: Framebuffering strategy, currently values of
``diff_to_previous`` or ``full_frame`` are only supported.
:type framebuffer: str
:param framebuffer: Framebuffering strategy, currently instances of
``diff_to_previous()`` or ``full_frame()`` are only supported.
:type framebuffer: luma.core.framebuffer.framebuffer
:param bgr: Set to ``True`` if device pixels are BGR order (rather than RGB).
:type bgr: bool
:param h_offset: Horizontal offset (in pixels) of screen to device memory
Expand All @@ -541,11 +553,11 @@ class ili9341(backlit_device):
.. versionadded:: 2.2.0
"""
def __init__(self, serial_interface=None, width=320, height=240, rotate=0,
framebuffer="diff_to_previous", h_offset=0, v_offset=0,
framebuffer=diff_to_previous(num_segments=25), h_offset=0, v_offset=0,
bgr=False, **kwargs):
super(ili9341, self).__init__(luma.lcd.const.ili9341, serial_interface, **kwargs)
self.capabilities(width, height, rotate, mode="RGB")
self.framebuffer = getattr(luma.core.framebuffer, framebuffer)(self)
self.init_framebuffer(framebuffer)

if h_offset != 0 or v_offset != 0:
def offset(bbox):
Expand Down Expand Up @@ -608,14 +620,14 @@ def display(self, image):

image = self.preprocess(image)

if self.framebuffer.redraw_required(image):
left, top, right, bottom = self.apply_offsets(self.framebuffer.bounding_box)
for image, bounding_box in self.framebuffer.redraw(image):
left, top, right, bottom = self.apply_offsets(bounding_box)

self.command(0x2a, left >> 8, left & 0xff, (right - 1) >> 8, (right - 1) & 0xff) # Set column addr
self.command(0x2b, top >> 8, top & 0xff, (bottom - 1) >> 8, (bottom - 1) & 0xff) # Set row addr
self.command(0x2c) # Memory write

self.data(self.framebuffer.image.crop(self.framebuffer.bounding_box).tobytes())
self.data(image.tobytes())

def contrast(self, level):
"""
Expand Down Expand Up @@ -828,7 +840,7 @@ def contrast(self, value):
self.command(0x81, value >> 2)


class hd44780(backlit_device, parallel_device, character):
class hd44780(backlit_device, parallel_device, character, __framebuffer_mixin):
"""
Driver for a HD44780 style LCD display. This class provides a ``text``
property which can be used to set and get a text value, which will be
Expand Down Expand Up @@ -868,9 +880,9 @@ class hd44780(backlit_device, parallel_device, character):
is connected to the backlight. This is unnecessary if it has already been
configured on the interface.
:type backpack_pin: int
:param framebuffer: Framebuffering strategy, currently values of
``diff_to_previous`` or ``full_frame`` are only supported.
:type framebuffer: str
:param framebuffer: Framebuffering strategy, currently instances of
``diff_to_previous()`` or ``full_frame()`` are only supported.
:type framebuffer: luma.core.framebuffer.framebuffer

To place text on the display, simply assign the text to the ``text``
instance variable::
Expand All @@ -889,7 +901,7 @@ class hd44780(backlit_device, parallel_device, character):
"""
def __init__(self, serial_interface=None, width=16, height=2, undefined='_',
selected_font=0, exec_time=0.000001,
framebuffer="diff_to_previous", **kwargs):
framebuffer=diff_to_previous(num_segments=1), **kwargs):
super(hd44780, self).__init__(luma.lcd.const.hd44780, serial_interface,
exec_time=exec_time, **kwargs)

Expand All @@ -898,7 +910,7 @@ def __init__(self, serial_interface=None, width=16, height=2, undefined='_',
self._exec_time = exec_time

self.capabilities(width * 5, height * 8, 0)
self.framebuffer = getattr(luma.core.framebuffer, framebuffer)(self)
self.init_framebuffer(framebuffer)

# Currently only support 5x8 fonts for the hd44780
self.font = embedded_fonts(self._const.FONTDATA,
Expand Down Expand Up @@ -1004,21 +1016,19 @@ def display(self, image):
assert(image.mode == self.mode)
assert(image.size == self.size)

if self.framebuffer.redraw_required(image):
self._cleanup_custom(image)
for image_segment, bounding_box in self.framebuffer.redraw(image):
self._cleanup_custom(image_segment)
# Expand bounding box to align to cell boundaries (5,8)
left, top, right, bottom = self.framebuffer.bounding_box
left, top, right, bottom = bounding_box
left = left // 5 * 5
right = right // 5 * 5 if not right % 5 else (right // 5 + 1) * 5
top = top // 8 * 8
bottom = bottom // 8 * 8 if not bottom % 8 else (bottom // 8 + 1) * 8
self.framebuffer.bounding_box = (left, top, right, bottom)

for j in range(top // 8, bottom // 8):
buf = []
for i in range(left // 5, right // 5):
img = self.framebuffer.image.crop((i * 5, j * 8,
(i + 1) * 5, (j + 1) * 8))
img = image.crop((i * 5, j * 8, (i + 1) * 5, (j + 1) * 8))
bytes = img.tobytes()
c = self.glyph_index[bytes] if bytes in self.glyph_index else \
self._custom[bytes] if bytes in self._custom else None
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def find_version(*file_paths):
namespace_packages=["luma"],
packages=find_packages(),
zip_safe=False,
install_requires=["luma.core>=1.16.2,<2.0.0"],
install_requires=["luma.core>=2.0.0"],
setup_requires=pytest_runner,
tests_require=test_deps,
extras_require={
Expand Down
23 changes: 13 additions & 10 deletions tests/test_hd44780.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from luma.lcd.device import hd44780
from luma.core.render import canvas
from luma.core.util import bytes_to_nibbles
from luma.core.framebuffer import full_frame, diff_to_previous
from luma.lcd.const import hd44780 as CONST

from PIL import Image, ImageDraw
Expand All @@ -23,7 +24,7 @@ def test_init_4bitmode():
"""
Test initialization of display using 4 bit mode
"""
hd44780(interface, gpio=gpio)
hd44780(interface, gpio=gpio, framebuffer=full_frame())

to_8 = \
[call(0x3), call(0x3), call(0x3, 0x3)] * 3
Expand Down Expand Up @@ -58,7 +59,7 @@ def test_init_8bitmode():
Test initialization of display using 4 bit mode
"""
interface._bitmode = 8
hd44780(interface, gpio=gpio)
hd44780(interface, gpio=gpio, framebuffer=full_frame())

to_8 = \
[call(0x30)] * 3
Expand Down Expand Up @@ -90,7 +91,7 @@ def test_display():
Test the display with a line of text and a rectangle to demonstrate correct
functioning of the auto-create feature
"""
device = hd44780(interface, bitmode=8, gpio=gpio, framebuffer="full_frame")
device = hd44780(interface, bitmode=8, gpio=gpio, framebuffer=full_frame())
interface.reset_mock()

# Use canvas to create a screen worth of data
Expand Down Expand Up @@ -120,7 +121,7 @@ def test_custom_full():
"""
Auto-create feature runs out of custom character space
"""
device = hd44780(interface, bitmode=8, gpio=gpio, framebuffer="diff_to_previous")
device = hd44780(interface, bitmode=8, gpio=gpio, framebuffer=diff_to_previous(num_segments=1))

# Consume 8 special character positions
img = Image.new('1', (80, 16), 0)
Expand All @@ -135,15 +136,17 @@ def test_custom_full():
drw.line((75, 8, 79, 15), fill='white')
device.display(img)

interface.assert_has_calls([call.command(0xcf), call.data([0x5f])])
interface.assert_has_calls([
call.command(0x40), call.data([0x10, 0x08, 0x08, 0x04, 0x04, 0x02, 0x02, 0x01]),
call.command(0xcf), call.data([0x0])])


def test_get_font():
"""
Test get font capability by requesting two fonts and printing a single
character from each that will be different between the two fonts
"""
device = hd44780(interface, bitmode=8, gpio=gpio)
device = hd44780(interface, bitmode=8, gpio=gpio, framebuffer=full_frame())

img = Image.new('1', (10, 8), 0)
a00 = device.get_font(0)
Expand All @@ -163,7 +166,7 @@ def test_no_contrast():
"""
HD44780 should ignore requests to change contrast
"""
device = hd44780(interface, bitmode=8, gpio=gpio)
device = hd44780(interface, bitmode=8, gpio=gpio, framebuffer=full_frame())
device.contrast(100)


Expand All @@ -179,7 +182,7 @@ def _mask(pin):
return 1 << pin

interface = Mock(unsafe=True, _bitmode=4, _backlight_enabled=0, _mask=_mask)
hd44780(interface, bitmode=8, backpack_pin=3, gpio=gpio)
hd44780(interface, bitmode=8, backpack_pin=3, gpio=gpio, framebuffer=full_frame())

assert interface._backlight_enabled == 8

Expand All @@ -192,7 +195,7 @@ def test_i2c_does_not_support_backlight():
interface = Mock(spec_set=luma.core.interface.serial.i2c)
flag = False
try:
hd44780(interface, gpio=gpio, backpack_pin=3)
hd44780(interface, gpio=gpio, backpack_pin=3, framebuffer=full_frame())
except luma.core.error.UnsupportedPlatform as ex:
assert str(ex) == "This I2C interface does not support a backlight"
flag = True
Expand All @@ -206,6 +209,6 @@ def test_unsupported_display_mode():
"""
import luma.core
try:
hd44780(interface, width=12, height=3, gpio=gpio)
hd44780(interface, width=12, height=3, gpio=gpio, framebuffer=full_frame())
except luma.core.error.DeviceDisplayModeError as ex:
assert str(ex) == "Unsupported display mode: 12 x 3"
13 changes: 7 additions & 6 deletions tests/test_ili9341.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from luma.lcd.device import ili9341
from luma.core.render import canvas
from luma.core.framebuffer import full_frame

from baseline_data import get_reference_data, primitives
from helpers import serial, setup_function, assert_invalid_dimensions # noqa: F401
Expand All @@ -29,7 +30,7 @@ def command(*cmd):
serial.command.side_effect = command
serial.data.side_effect = data

ili9341(serial, gpio=Mock())
ili9341(serial, gpio=Mock(), framebuffer=full_frame())

assert serial.data.called
assert serial.command.called
Expand Down Expand Up @@ -75,7 +76,7 @@ def command(*cmd):
serial.command.side_effect = command
serial.data.side_effect = data

ili9341(serial, gpio=Mock(), width=240, height=240)
ili9341(serial, gpio=Mock(), width=240, height=240, framebuffer=full_frame())

assert serial.data.called
assert serial.command.called
Expand Down Expand Up @@ -121,7 +122,7 @@ def command(*cmd):
serial.command.side_effect = command
serial.data.side_effect = data

ili9341(serial, gpio=Mock(), width=320, height=180)
ili9341(serial, gpio=Mock(), width=320, height=180, framebuffer=full_frame())

assert serial.data.called
assert serial.command.called
Expand Down Expand Up @@ -175,7 +176,7 @@ def command(*cmd):
serial.command.side_effect = command
serial.data.side_effect = data

ili9341(serial, gpio=Mock(), width=240, height=240, h_offset=2, v_offset=1)
ili9341(serial, gpio=Mock(), width=240, height=240, h_offset=2, v_offset=1, framebuffer=full_frame())

assert serial.data.called
assert serial.command.called
Expand Down Expand Up @@ -230,8 +231,8 @@ def test_show():
serial.command.assert_called_once_with(41)


def test_display():
device = ili9341(serial, gpio=Mock())
def test_display_full_frame():
device = ili9341(serial, gpio=Mock(), framebuffer=full_frame())
serial.reset_mock()

recordings = []
Expand Down