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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for ST7565 LCD displays #170

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ Contributors
* Maciej Sokolowski (@matemaciek)
* Frederic Meeuwissen (@Frederic98)
* Ed Wightman (@wightmanjr)
* Hannes Campidell (@hannescam)
Binary file added doc/tech-spec/ST7565.pdf
Binary file not shown.
5 changes: 5 additions & 0 deletions luma/lcd/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ class st7567(object):
DISPLAYOFF = 0xAE


class st7565(object):
DISPLAYON = 0xAF
DISPLAYOFF = 0xAE


class pcd8544(object):
DISPLAYON = 0x0C
DISPLAYOFF = 0x08
Expand Down
84 changes: 83 additions & 1 deletion luma/lcd/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@
from luma.core.virtual import character
from luma.core.bitmap_font import embedded_fonts


__all__ = [
"pcd8544", "st7735", "st7789", "ht1621", "uc1701x", "st7567", "ili9341",
"pcd8544", "st7735", "st7565", "st7567", "st7789", "ht1621", "uc1701x", "ili9341",
"ili9486", "ili9488", "hd44780"
]

Expand Down Expand Up @@ -465,6 +466,87 @@ def init_framebuffer(self, framebuffer, default_num_segments):
self.framebuffer = framebuffer


class st7565(backlit_device):
"""
Serial interface to a monochrome ST7565 128x64 pixel LCD display.

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.

:param serial_interface: The serial interface (usually a
:py:class:`luma.core.interface.serial.spi` instance) to delegate sending
data and commands through.
:param rotate: An integer value of 0 (default), 1, 2 or 3 only, where 0 is
no rotation, 1 is rotate 90° clockwise, 2 is 180° rotation and 3
represents 270° rotation.
:type rotate: int

.. versionadded:: 2.12.0
"""

def __init__(self, serial_interface=None, rotate=0, **kwargs):
super(st7565, self).__init__(luma.lcd.const.st7565, serial_interface, **kwargs)
self.capabilities(128, 64, rotate)

self._pages = self._h // 8

self.command(0xA2) # Bias 1/9
self.command(0xA0)
self.command(0xC8)
self.command(0xA6)
self.command(0x2F)
self.command(0x2F)
self.command(0x22)
self.command(0xAF)

self.contrast(0x21)

self.clear()
self.show()

def display(self, image):
"""
Takes a 1-bit :py:mod:`PIL.Image` and dumps it to the ST7565
LCD display
"""
assert image.mode == self.mode
assert image.size == self.size

image = self.preprocess(image)

set_page_address = 0xB0

image_data = image.getdata()
pixels_per_page = self.width * 8
buf = bytearray(self.width)

for y in range(0, int(self._pages * pixels_per_page), pixels_per_page):
self.command(set_page_address, 0x00, 0x10)
set_page_address += 1
offsets = [y + self.width * i for i in range(8)]

for x in range(self.width):
buf[x] = \
(image_data[x + offsets[0]] and 0x01) | \
(image_data[x + offsets[1]] and 0x02) | \
(image_data[x + offsets[2]] and 0x04) | \
(image_data[x + offsets[3]] and 0x08) | \
(image_data[x + offsets[4]] and 0x10) | \
(image_data[x + offsets[5]] and 0x20) | \
(image_data[x + offsets[6]] and 0x40) | \
(image_data[x + offsets[7]] and 0x80)

self.data(list(buf))

def contrast(self, value):
"""
Sets the LCD contrast
"""
assert 0 <= value <= 255
self.command(0x81, value)


class st7735(backlit_device, __framebuffer_mixin):
"""
Serial interface to a 262K color (6-6-6 RGB) ST7735 LCD display.
Expand Down
73 changes: 73 additions & 0 deletions tests/test_st7565.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2013-17 Richard Hull and contributors
# See LICENSE.rst for details.

"""
Tests for the :py:class:`luma.lcd.device.st7565` device.
"""

import pytest

from luma.lcd.device import st7565
from luma.core.render import canvas

from baseline_data import get_reference_data, primitives
from helpers import serial, setup_function, assert_invalid_dimensions # noqa: F401
from unittest.mock import Mock, call


def test_init_128x64():
st7565(serial, gpio=Mock())
serial.command.assert_has_calls([
call(0xA2),
call(0xA0),
call(0xC8),
call(0xA6),
call(0x2f),
call(0x2F),
call(0x22),
call(0xAF),
call(0x81, 21)
])

# Next 1024 are all data: zeros to clear the RAM
# (1024 = 128 * 64 / 8)
serial.data.assert_has_calls([call([0] * 128)] * 8)


def test_contrast():
device = st7565(serial, gpio=Mock())
serial.reset_mock()
with pytest.raises(AssertionError):
device.contrast(300)


def test_display():
device = st7565(serial, gpio=Mock())
serial.reset_mock()

recordings = []

def data(data):
recordings.append({'data': data})

def command(*cmd):
recordings.append({'command': list(cmd)})

serial.command.side_effect = command
serial.data.side_effect = data

# Use the same drawing primitives as the demo
with canvas(device) as draw:
primitives(device, draw)

assert serial.data.called
assert serial.command.called

# To regenerate test data, uncomment the following (remember not to commit though)
# ================================================================================
# from baseline_data import save_reference_data
# save_reference_data("demo_st7565", recordings)

assert recordings == get_reference_data('demo_st7565')
Loading