# -*- coding: utf-8 -*-
# Copyright (c) 2013-2023 Richard Hull and contributors
# See LICENSE.rst for details.
"""
Collection of serial interfaces to LCD devices.
"""
# Example usage:
#
# from luma.core.interface.serial import spi
# from luma.core.render import canvas
# from luma.lcd.device import pcd8544
# from PIL import ImageDraw
#
# serial = spi(port=0, device=0)
# device = pcd8544(serial)
#
# with canvas(device) as draw:
# draw.rectangle(device.bounding_box, outline="white", fill="black")
# draw.text(30, 40, "Hello World", fill="white")
#
# 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 time import sleep
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
from luma.lcd.segment_mapper import dot_muncher
from luma.core.virtual import character
from luma.core.bitmap_font import embedded_fonts
__all__ = [
"pcd8544", "st7735", "st7789", "ht1621", "uc1701x", "st7567", "ili9341",
"ili9486", "ili9488", "hd44780"
]
class GPIOBacklight:
"""
Helper class for controlling the LCD backlight using a digital GPIO output pin.
:param gpio: GPIO interface (must be compatible with `RPi.GPIO <https://pypi.python.org/pypi/RPi.GPIO>`_).
:param pin: the GPIO pin to use for the backlight.
:type pin: int
:param active_low: Set to True if active low (default), False otherwise.
:type active_low: bool
:raises luma.core.error.UnsupportedPlatform: GPIO access not available.
.. versionadded:: 2.3.0
"""
def __init__(self, gpio, pin=18, active_low=True):
self._gpio = gpio
self._pin = pin
if active_low:
self._enabled = self._gpio.LOW
self._disabled = self._gpio.HIGH
else:
self._enabled = self._gpio.HIGH
self._disabled = self._gpio.LOW
try:
self._gpio.setup(self._pin, self._gpio.OUT)
except RuntimeError as e:
if str(e) == 'Module not imported correctly!':
raise luma.core.error.UnsupportedPlatform('GPIO access not available')
def __call__(self, is_enabled):
"""
Toggle the LCD Backlight
:param is_enabled: Turn backlight on or off.
:type is_enabled: bool
"""
assert is_enabled in (True, False)
self._gpio.output(self._pin, self._enabled if is_enabled else self._disabled)
def cleanup(self):
if self._gpio:
self._gpio.cleanup(self._pin)
class I2CBackpackBacklight:
"""
Helper class for controlling the LCD backlight when the device is using
an I2C backpack such as the PCF8574
:param serial_interface: The serial interface (usually a
:py:class:`luma.core.interface.serial.pcf8574` instance) that the device
uses to communicate.
:param pin: the backpack pin number used to control the backlight on the
device.
:type pin: int
:raises luma.core.error.UnsupportedPlatform: This I2C interfaces does not
support a backlight.
.. note: If the I2C interface is already configured to use a backlight, the
pin setting here will be ignored.
.. versionadded:: 2.5.0
"""
def __init__(self, serial_interface, pin=None):
self._serial_interface = serial_interface
if not hasattr(serial_interface, "_backlight_enabled"):
raise luma.core.error.UnsupportedPlatform('This I2C interface does not support a backlight')
# If the serial_interface has already been set up to use a backlight
# use its setting, otherwise initialize the backlight from the pin arg
_be = serial_interface._backlight_enabled
self._pin = _be if _be else serial_interface._mask(pin) if pin else 0
serial_interface._backlight_enabled = self._pin
def __call__(self, is_enabled):
"""
Toggle the LCD Backlight
:param is_enabled: Turn on or off the backlight.
:type is_enabled: bool
"""
assert is_enabled in (True, False)
self._serial_interface._backlight_enabled = self._pin if is_enabled else 0
class PWMBacklight:
"""
Helper class for controlling the LCD backlight using a PWM channel pin.
:param gpio: GPIO interface (must be compatible with `RPi.GPIO <https://pypi.python.org/pypi/RPi.GPIO>`_).
:param pin: the GPIO pin to use for the backlight.
:type pin: int
:param frequency: The frequency to use for the PWM channel.
:type frequency: float
:raises luma.core.error.UnsupportedPlatform: GPIO access not available.
.. versionadded:: 2.3.0
"""
def __init__(self, gpio, pin=18, frequency=362):
self._gpio = gpio
try:
self._pwm = self._gpio.PWM(pin, frequency)
self._pwm.start(0.0)
self._pin = pin
except RuntimeError as e:
if str(e) == 'Module not imported correctly!':
raise luma.core.error.UnsupportedPlatform('GPIO access not available')
def __call__(self, value):
"""
Set the LCD Backlight
:param value: Sets the value of the backlight. Can provide a bool
(True/False) to turn on/off or a float to set the backlight intensity in
percentage (0 <= value <= 100.0).
:type value: bool or float
"""
if value in (True, False):
value = 100.0 if value else 0.0
assert 0.0 <= value <= 100.0
self._pwm.ChangeDutyCycle(value)
def cleanup(self):
if self._gpio:
self._pwm.stop()
self._gpio.cleanup(self._pin)
@rpi_gpio
class backlit_device(device):
"""
Controls a backlight (active low), assumed to be on GPIO 18 (``PWM_CLK0``) by default.
:param gpio: GPIO interface (must be compatible with `RPi.GPIO <https://pypi.python.org/pypi/RPi.GPIO>`_).
:param gpio_LIGHT: The GPIO pin to use for the backlight.
:type gpio_LIGHT: int
:param active_low: Set to true if active low (default), false otherwise.
:type active_low: bool
:param pwm_frequency: Use PWM for backlight brightness control with the specified frequency when provided.
:type pwm_frequency: float
:type backpack_pin: If using an I2C backpack, sets the pin on the backpack that
is connected to the backlight.
:type backpack_pin: int
:raises luma.core.error.UnsupportedPlatform: GPIO access not available.
.. versionadded:: 2.0.0
"""
def __init__(self, const=None, serial_interface=None, gpio=None, gpio_LIGHT=18, active_low=True, pwm_frequency=None, backpack_pin=None, **kwargs):
super(backlit_device, self).__init__(const, serial_interface)
if backpack_pin or (isinstance(serial_interface, pcf8574) and hasattr(serial_interface, "_backlight_enabled")):
self.backlight = I2CBackpackBacklight(serial_interface, pin=backpack_pin)
elif pwm_frequency:
self._gpio = gpio or self.__rpi_gpio__()
self.backlight = PWMBacklight(self._gpio, pin=gpio_LIGHT, frequency=pwm_frequency)
else:
self._gpio = gpio or self.__rpi_gpio__()
self.backlight = GPIOBacklight(self._gpio, pin=gpio_LIGHT, active_low=active_low)
self.persist = True
self.backlight(True)
def cleanup(self):
"""
Attempt to reset the device & switching it off prior to exiting the
python process.
"""
if self.persist:
self.backlight(False)
try:
self.backlight.cleanup()
except AttributeError:
# Not all backlights require cleanup
pass
super(backlit_device, self).cleanup()
[docs]
class pcd8544(backlit_device):
"""
Serial interface to a monochrome PCD8544 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
"""
def __init__(self, serial_interface=None, rotate=0, **kwargs):
super(pcd8544, self).__init__(luma.lcd.const.pcd8544, serial_interface, **kwargs)
self.capabilities(84, 48, rotate)
self._mask = [1 << (i // self._w) % 8 for i in range(self._w * self._h)]
self._offsets = [(self._w * (i // (self._w * 8))) + (i % self._w) for i in range(self._w * self._h)]
self.contrast(0xB0)
self.clear()
self.show()
[docs]
def display(self, image):
"""
Takes a 1-bit :py:mod:`PIL.Image` and dumps it to the PCD8544
LCD display.
"""
assert image.mode == self.mode
assert image.size == self.size
image = self.preprocess(image)
self.command(0x20, 0x80, 0x40)
buf = bytearray(self._w * self._h // 8)
off = self._offsets
mask = self._mask
for idx, pix in enumerate(image.getdata()):
if pix > 0:
buf[off[idx]] |= mask[idx]
self.data(list(buf))
[docs]
def contrast(self, value):
"""
Sets the LCD contrast
"""
assert 0 <= value <= 255
self.command(0x21, 0x14, value | 0x80, 0x20)
[docs]
class st7789(backlit_device):
"""
Serial interface to a colour ST7789 240x240 pixel LCD display.
.. versionadded:: 2.9.0
"""
def __init__(self, serial_interface=None, width=240, height=240, rotate=0, **kwargs):
super(st7789, self).__init__(luma.lcd.const.st7789, serial_interface, **kwargs)
self.capabilities(width, height, rotate, mode="RGB")
self.command(0x36, 0x70) # MADCTL (36h): Memory Data Access Control: Bottom to Top, Right to Left, Reverse Mode
self.command(0x3A, 0x06) # COLMOD (3Ah): Interface Pixel Format: 18bit/pixel
self.command(0xB2, # PORCTRL (B2h): Porch Setting: Disable separate porch control, 0xC in normal mode, 0x3 in idle and partial modes
0x0C, 0x0C, 0x00, 0x33, 0x33)
self.command(0xB7, 0x35) # GCTRL (B7h): Gate Control: VGH = 13.26V, VGL = -10.43V
self.command(0xBB, 0x19) # VCOMS (BBh): VCOM Setting: 0.725V
self.command(0xC0, 0x2C) # LCMCTRL (C0h): LCM Control: XBGR, XMX, XMH
self.command(0xC2, 0x01) # VDVVRHEN (C2h): VDV and VRH Command Enable: VDV and VRH register value comes from command write
self.command(0xC3, 0x12) # VRHS (C3h): VRH Set: 4.45V + (vcom + vcom offset + vdv)
self.command(0xC4, 0x20) # VDVS (C4h): VDV Set: 0V
self.command(0xC6, 0x0F) # FRCTRL2 (C6h): Frame Rate Control in Normal Mode: 60Hz
self.command(0xD0, # PWCTRL1 (D0h): Power Control 1: AVDD = 6.8V, AVCL = -4.8V, VDDS = 2.3V
0xA4, 0xA1)
self.command(0xE0, # PVGAMCTRL (E0h): Positive Voltage Gamma Control
0xD0, 0x04, 0x0D, 0x11, 0x13, 0x2B, 0x3F, 0x54, 0x4C, 0x18, 0x0D, 0x0B, 0x1F, 0x23)
self.command(0xE1, # NVGAMCTRL (E1h): Negative Voltage Gamma Control
0xD0, 0x04, 0x0C, 0x11, 0x13, 0x2C, 0x3F, 0x44, 0x51, 0x2F, 0x1F, 0x1F, 0x20, 0x23)
self.command(0x21) # INVON (21h): Display Inversion On
self.command(0x11) # SLPOUT (11h): Sleep Out
self.command(0x29) # DISPON (29h): Display On
self.clear()
self.show()
[docs]
def command(self, cmd, *args):
"""Send a command to the display, with optional arguments.
The arguments are sent as data bytes, in accordance with the ST7789 datasheet."""
super(st7789, self).command(cmd)
if args:
self.data(args)
[docs]
def set_window(self, x1, y1, x2, y2):
self.command(0x2A, # CASET (2Ah): Column Address Set
x1 >> 8, x1 & 0xFF, (x2 - 1) >> 8, (x2 - 1) & 0xFF)
self.command(0x2B, # RASET (2Bh): Row Address Set
y1 >> 8, y1 & 0xFF, (y2 - 1) >> 8, (y2 - 1) & 0xFF)
self.command(0x2C) # RAMWR (2Ch): Memory Write
[docs]
def display(self, image):
self.set_window(0, 0, self._w, self._h)
image = self.preprocess(image)
self.data(list(image.convert("RGB").tobytes()))
[docs]
def contrast(self, level):
"""
NOT SUPPORTED
:param level: Desired contrast level in the range of 0-255.
:type level: int
"""
assert 0 <= level <= 255
[docs]
class st7567(backlit_device):
"""
Serial interface to a monochrome ST7567 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:: 1.1.0
"""
def __init__(self, serial_interface=None, rotate=0, **kwargs):
super(st7567, self).__init__(luma.lcd.const.st7567, serial_interface, **kwargs)
self.capabilities(128, 64, rotate)
self._pages = self._h // 8
self.command(0xA3) # Bias 1/7
self.command(0xA1)
self.command(0xC0) # Normal Orientation
self.command(0xA6) # Normal Display (0xA7 = inverse)
self.command(0x40)
self.command(0x2F)
self.command(0x22)
self.command(0xAF)
self.contrast(57)
self.clear()
self.show()
[docs]
def display(self, image):
"""
Takes a 1-bit :py:mod:`PIL.Image` and dumps it to the ST7567
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, 0x04, 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))
[docs]
def contrast(self, value):
"""
Sets the LCD contrast
"""
assert 0 <= value <= 255
self.command(0x81, value)
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, default_num_segments):
if framebuffer is None:
self.framebuffer = diff_to_previous(num_segments=default_num_segments)
elif 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
[docs]
class st7735(backlit_device, __framebuffer_mixin):
"""
Serial interface to a 262K color (6-6-6 RGB) ST7735 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 width: The number of pixels laid out horizontally.
:type width: int
:param height: The number of pixels laid out vertically.
:type height: int
: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
: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.
:type inverse: bool
:param h_offset: Horizontal offset (in pixels) of screen to device memory
(default: 0).
:type h_offset: int
:param v_offset: Vertical offset (in pixels) of screen to device memory
(default: 0).
:type v_offset: int
.. versionadded:: 0.3.0
"""
def __init__(self, serial_interface=None, width=160, height=128, rotate=0,
framebuffer=None, 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.init_framebuffer(framebuffer, 16)
if h_offset != 0 or v_offset != 0:
def offset(bbox):
left, top, right, bottom = bbox
return (left + h_offset, top + v_offset, right + h_offset, bottom + v_offset)
self.apply_offsets = offset
else:
self.apply_offsets = lambda bbox: bbox
# Supported modes
supported = (width, height) in [
(160, 80),
(160, 128),
(128, 128)
]
if not supported:
raise luma.core.error.DeviceDisplayModeError(
f"Unsupported display mode: {width} x {height}")
# RGB or BGR order
order = 0x08 if bgr else 0x00
self.command(0x01) # reset
self.command(0x11) # sleep out & booster on
self.command(0xB1, 0x01, 0x2C, 0x2D) # frame rate control: normal mode
self.command(0xB2, 0x01, 0x2C, 0x2D) # frame rate control: idle mode
self.command(0xB3, 0x01, 0x2C, 0x2D, # frame rate control: partial mode dot inversion mode
0x01, 0x2C, 0x2D) # frame rate control: line inversion mode
self.command(0xB4, 0x07) # display inversion: none
self.command(0xC0, 0xA2, 0x02, 0x84) # power control 1: -4.6V auto mode
self.command(0xC1, 0xC5) # power control 2: VGH
self.command(0xC2, 0x0A, 0x00) # power control 3: OpAmp current small, boost freq
self.command(0xC3, 0x8A, 0x2A) # power control 4: BCLK/2, Opamp current small & Medium low
self.command(0xC4, 0x8A, 0xEE) # power control 5: partial mode/full-color
self.command(0xC5, 0x0E) # VCOM Control 1
self.command(0x36, 0x60 | order) # memory data access control
self.command(0x21 if inverse else 0x20) # display inversion on(0x21)/off(0x20)
self.command(0x3A, 0x06) # interface pixel format
self.command(0x13) # partial off (normal)
self.command(0xE0, # gamma adjustment (+ polarity)
0x0F, 0x1A, 0x0F, 0x18, 0x2F, 0x28, 0x20, 0x22,
0x1F, 0x1B, 0x23, 0x37, 0x00, 0x07, 0x02, 0x10)
self.command(0xE1, # gamma adjustment (- polarity)
0x0F, 0x1B, 0x0F, 0x17, 0x33, 0x2C, 0x29, 0x2E,
0x30, 0x30, 0x39, 0x3F, 0x00, 0x07, 0x03, 0x10)
self.clear()
self.show()
[docs]
def display(self, image):
"""
Renders a 24-bit RGB image to the ST7735 LCD display. The 8-bit RGB
values are passed directly to the devices internal storage, but only
the 6 most-significant bits are used by the display.
:param image: The image to render.
:type image: PIL.Image.Image
"""
assert image.mode == self.mode
assert image.size == self.size
image = self.preprocess(image)
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(list(image.tobytes()))
[docs]
def contrast(self, level):
"""
NOT SUPPORTED
:param level: Desired contrast level in the range of 0-255.
:type level: int
"""
assert 0 <= level <= 255
[docs]
def command(self, cmd, *args):
"""
Sends a command and an (optional) sequence of arguments through to the
delegated serial interface. Note that the arguments are passed through
as data.
"""
self._serial_interface.command(cmd)
if len(args) > 0:
self._serial_interface.data(list(args))
[docs]
class ili9341(backlit_device, __framebuffer_mixin):
"""
Serial interface to a 262k color (6-6-6 RGB) ILI9341 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 width: The number of pixels laid out horizontally.
:type width: int
:param height: The number of pixels laid out vertically.
:type height: int
: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
: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
(default: 0).
:type h_offset: int
:param v_offset: Vertical offset (in pixels) of screen to device memory
(default: 0).
:type v_offset: int
.. versionadded:: 2.2.0
"""
def __init__(self, serial_interface=None, width=320, height=240, rotate=0,
framebuffer=None, 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.init_framebuffer(framebuffer, 25)
if h_offset != 0 or v_offset != 0:
def offset(bbox):
left, top, right, bottom = bbox
return (left + h_offset, top + v_offset, right + h_offset, bottom + v_offset)
self.apply_offsets = offset
else:
self.apply_offsets = lambda bbox: bbox
# Supported modes
supported = (width, height) in [
(320, 240), # full
(240, 240), # 1x1
(320, 180) # 16x9
]
if not supported:
raise luma.core.error.DeviceDisplayModeError(
f"Unsupported display mode: {width} x {height}")
# RGB or BGR order
order = 0x00 if bgr else 0x08
# Note: based on the Adafruit implementation at
# `https://github.com/adafruit/Adafruit_CircuitPython_RGB_Display` (MIT licensed)
self.command(0xef, 0x03, 0x80, 0x02) # ?
self.command(0xcf, 0x00, 0xc1, 0x30) # Power control B
self.command(0xed, 0x64, 0x03, 0x12, 0x81) # Power on sequence control
self.command(0xe8, 0x85, 0x00, 0x78) # Driver timing control A
self.command(0xcb, 0x39, 0x2c, 0x00, 0x34, 0x02) # Power control A
self.command(0xf7, 0x20) # Pump ratio control
self.command(0xea, 0x00, 0x00) # Driver timing control B
self.command(0xc0, 0x23) # Power Control 1, VRH[5:0]
self.command(0xc1, 0x10) # Power Control 2, SAP[2:0], BT[3:0]
self.command(0xc5, 0x3e, 0x28) # VCM Control 1
self.command(0xc7, 0x86) # VCM Control 2
self.command(0x36, 0x20 | order) # Memory Access Control
self.command(0x3a, 0x46) # Pixel Format 6-6-6
self.command(0xb1, 0x00, 0x18) # FRMCTR1
self.command(0xb6, 0x08, 0x82, 0x27) # Display Function Control
self.command(0xf2, 0x00) # 3Gamma Function Disable
self.command(0x26, 0x01) # Gamma Curve Selected
self.command(0xe0, # Set Gamma (+ polarity)
0x0f, 0x31, 0x2b, 0x0c, 0x0e, 0x08, 0x4e, 0xf1,
0x37, 0x07, 0x10, 0x03, 0x0e, 0x09, 0x00)
self.command(0xe1, # Set Gamma (- polarity)
0x00, 0x0e, 0x14, 0x03, 0x11, 0x07, 0x31, 0xc1,
0x48, 0x08, 0x0f, 0x0c, 0x31, 0x36, 0x0f)
self.command(0x11) # Sleep out
sleep(0.12)
self.clear()
self.show()
[docs]
def display(self, image):
"""
Renders a 24-bit RGB image to the ILI9341 LCD display. The 8-bit RGB
values are passed directly to the devices internal storage, but only
the 6 most-significant bits are used by the display.
:param image: The image to render.
:type image: PIL.Image.Image
"""
assert image.mode == self.mode
assert image.size == self.size
image = self.preprocess(image)
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(image.tobytes())
[docs]
def contrast(self, level):
"""
NOT SUPPORTED
:param level: Desired contrast level in the range of 0-255.
:type level: int
"""
assert 0 <= level <= 255
[docs]
def command(self, cmd, *args):
"""
Sends a command and an (optional) sequence of arguments through to the
delegated serial interface. Note that the arguments are passed through
as data.
"""
self._serial_interface.command(cmd)
if len(args) > 0:
self._serial_interface.data(list(args))
[docs]
class ili9486(backlit_device, __framebuffer_mixin):
"""
Serial interface to a 262k color (6-6-6 RGB) ILI9486 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 (if implemented) and other settings.
Note that the ILI9486 display used for development -- a Waveshare
3.5-inch IPS LCD(B) -- used a portrait orientation. Images were
rendered correctly only when specifying that height was 480 pixels
and the width was 320.
:param serial_interface: the serial interface (usually a
:py:class:`luma.core.interface.serial.spi` instance) to delegate sending
data and commands through.
:param width: The number of pixels laid out horizontally.
:type width: int
:param height: The number of pixels laid out vertically.
:type height: int
: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
: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
(default: 0).
:type h_offset: int
:param v_offset: Vertical offset (in pixels) of screen to device memory
(default: 0).
:type v_offset: int
.. versionadded:: 2.8.0
"""
def __init__(self, serial_interface=None, width=320, height=480, rotate=0,
framebuffer=None, h_offset=0, v_offset=0, bgr=False, **kwargs):
super(ili9486, self).__init__(luma.lcd.const.ili9486, serial_interface, **kwargs)
self.capabilities(width, height, rotate, mode="RGB")
self.init_framebuffer(framebuffer, 25)
if h_offset != 0 or v_offset != 0:
def offset(bbox):
left, top, right, bottom = bbox
return (left + h_offset, top + v_offset, right + h_offset, bottom + v_offset)
self.apply_offsets = offset
else:
self.apply_offsets = lambda bbox: bbox
# Supported modes
supported = (width, height) in [
(320, 480) # full
]
if not supported:
raise luma.core.error.DeviceDisplayModeError(
f"Unsupported display mode: {width} x {height}")
# RGB or BGR order
order = 0x00 if bgr else 0x08
# Initialization sequence, adapted from MIT Licensed
#
# `https://github.com/juj/fbcp-ili9341/blob/master/ili9486.cpp`
#
# and peer files. The sequence targets the ILI9486-based
# Waveshare "Wavepear" 3.5 inch 320x480 LCD (B)
# implementation. Per comments in the above file and issue
# discussion for juj/fbcp-ili9341, the Waveshare
# implementation makes use of 16-bit shift register for the
# SPI interface, leaving the ILI9486 itself in a parallel
# mode.
#
# The result of that implementation is that registers
# effectively become 16-bit quantities. The sequence below
# (and in the display() function) thus ends up padding
# commands and subsequent values. It doesn't seem to have
# affected pixel transfers, though.
#
# A different ILI9486 implementation may NOT need the padding
# zeros. The juj/fbcp-ili9341 code handles that possibility
# via a DISPLAY_SPI_BUS_IS_16BITS_WIDE ifdef.
self.command(0xb0, 0x00, 0x00) # Interface Mode Control
self.command(0x11) # sleep out
sleep(0.150)
self.command(0x3a, 0x00, 0x66) # Interface Pixel Format 6-6-6
self.command(0x21) # Display inversion ON for LCD(B)
self.command(0xc0, 0x00, 0x09, 0x00, 0x09) # Power Control 1
self.command(0xc1, 0x00, 0x41, 0x00, 0x00) # Power Control 2
self.command(0xc2, 0x00, 0x33) # Power Control 3 (for normal mode)
self.command(0xc5, 0x00, 0x00, 0x00, 0x36) # VCOM control
self.command(0x36, 0x00, 0x00 | order) # Memory Access control (MAD), rotations and color order
# self.command(0xb1, 0x00, 0xb0, 0x00, 0xe0) # Frame Rate Control (needed?)
self.command(0xb6, 0x00, 0x00, 0x00, 0x42, 0x00, 59) # Display Function Control
# Initial trials didn't seem to need Positive, Negative, or
# Digital Gamma Control settings.
self.command(0x13) # Normal mode ON
self.command(0x34) # Tearing effect line oFF
self.command(0x38) # Idle mode OFF
self.command(0x11) # sleep out
sleep(0.150)
self.clear()
self.show()
[docs]
def display(self, image):
"""
Renders a 24-bit RGB image to the ILI9486 LCD display. The 8-bit RGB
values are passed directly to the devices internal storage, but only
the 6 most-significant bits are used by the display.
:param image: The image to render.
:type image: PIL.Image.Image
"""
assert image.mode == self.mode
assert image.size == self.size
image = self.preprocess(image)
for image, bounding_box in self.framebuffer.redraw(image):
# Transposing the display shifts the dimension measurements
top, left, bottom, right = self.apply_offsets(bounding_box)
# Per earlier comments, Waveshare's display needs padding
# for commands.
self.command(0x2a, 0, top >> 8, 0, top & 0xff, 0, (bottom - 1) >> 8, 0, (bottom - 1) & 0xff) # Set row addr
self.command(0x2b, 0, left >> 8, 0, left & 0xff, 0, (right - 1) >> 8, 0, (right - 1) & 0xff) # Set column addr
self.command(0x2c) # Memory write
self.data(image.tobytes())
[docs]
def contrast(self, level):
"""
NOT SUPPORTED
:param level: Desired contrast level in the range of 0-255.
:type level: int
"""
assert 0 <= level <= 255
[docs]
def command(self, cmd, *args):
"""
Sends a command and an (optional) sequence of arguments through to the
delegated serial interface. Note that the arguments are passed through
as data.
"""
self._serial_interface.command(cmd)
if len(args) > 0:
self._serial_interface.data(list(args))
[docs]
class ili9488(backlit_device, __framebuffer_mixin):
"""
Serial interface to a 262k color (6-6-6 RGB) ILI9488 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 width: The number of pixels laid out horizontally.
:type width: int
:param height: The number of pixels laid out vertically.
:type height: int
: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
: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
(default: 0).
:type h_offset: int
:param v_offset: Vertical offset (in pixels) of screen to device memory
(default: 0).
:type v_offset: int
.. versionadded:: 2.11.0
"""
def __init__(self, serial_interface=None, width=480, height=320, rotate=0,
framebuffer=None, h_offset=0, v_offset=0, bgr=False, **kwargs):
super(ili9488, self).__init__(luma.lcd.const.ili9488, serial_interface, **kwargs)
self.capabilities(width, height, rotate, mode="RGB")
self.init_framebuffer(framebuffer, 25)
if h_offset != 0 or v_offset != 0:
def offset(bbox):
left, top, right, bottom = bbox
return (left + h_offset, top + v_offset, right + h_offset, bottom + v_offset)
self.apply_offsets = offset
else:
self.apply_offsets = lambda bbox: bbox
# Supported modes
supported = (width, height) in [(480, 320)]
if not supported:
raise luma.core.error.DeviceDisplayModeError(
f"Unsupported display mode: {width} x {height}")
# RGB or BGR order
order = 0x00 if bgr else 0x08
# Initialization sequence, adapted from
#
# https://github.com/birdtechstep/fbtft/blob/master/fb_ili9488.c
#
self.command(0xe0, 0x00, 0x03, 0x09, 0x08, 0x16, 0x0a, 0x37, # Positive Gamma Control
0x78, 0x4c, 0x09, 0x0a, 0x08, 0x16, 0x1a, 0x0f)
self.command(0xe1, 0x00, 0x16, 0x19, 0x03, 0x0f, 0x05, 0x32,
0x45, 0x46, 0x04, 0x0e, 0x0d, 0x35, 0x37, 0x0f) # Negative Gamma Control
self.command(0xc0, 0x17, 0x15) # Power Control 1
self.command(0xc1, 0x41) # Power Control 2
self.command(0xc5, 0x00, 0x12, 0x80) # VCOM Control
self.command(0x36, 0x20 | order) # Memory Access Control
self.command(0x3a, 0x66) # Interface Pixel Format 6-6-6
self.command(0xb0, 0x00) # Interface Mode Control
self.command(0xb1, 0xa0) # Frame Rate Control
self.command(0xb4, 0x02) # Display Inversion Control
self.command(0xb6, 0x02, 0x02, 0x3b) # Display Function Control
self.command(0xb7, 0xc6) # Entry Mode Set
self.command(0xf7, 0xa9, 0x51, 0x2c, 0x82) # Interface Mode Control
self.command(0x11) # Sleep Out
sleep(0.120)
self.clear()
self.show()
[docs]
def display(self, image):
"""
Renders a 24-bit RGB image to the ILI9488 LCD display. The 8-bit RGB
values are passed directly to the devices internal storage, but only
the 6 most-significant bits are used by the display.
:param image: The image to render.
:type image: PIL.Image.Image
"""
assert image.mode == self.mode
assert image.size == self.size
image = self.preprocess(image)
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(image.tobytes())
[docs]
def contrast(self, level):
"""
NOT SUPPORTED
:param level: Desired contrast level in the range of 0-255.
:type level: int
"""
assert 0 <= level <= 255
[docs]
def command(self, cmd, *args):
"""
Sends a command and an (optional) sequence of arguments through to the
delegated serial interface. Note that the arguments are passed through
as data.
"""
self._serial_interface.command(cmd)
if len(args) > 0:
self._serial_interface.data(list(args))
[docs]
@rpi_gpio
class ht1621(backlit_device):
"""
Serial interface to a seven segment HT1621 monochrome 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 gpio: The GPIO library to use (usually RPi.GPIO)
to delegate sending data and commands through.
:param width: The number of 7 segment characters laid out horizontally.
:type width: int
: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
:param WR: The write (SPI clock) pin to connect to, default BCM 11.
:type WR: int
:param DAT: The data pin to connect to, default BCM 10.
:type DAT: int
:param CS: The chip select pin to connect to, default BCM 8.
:type CS: int
.. versionadded:: 0.4.0
"""
def __init__(self, gpio=None, width=6, rotate=0, WR=11, DAT=10, CS=8, **kwargs):
if 'serial_interface' in kwargs:
del kwargs['serial_interface']
super(ht1621, self).__init__(luma.lcd.const.ht1621, noop(), gpio=gpio, **kwargs)
self.capabilities(width, 8, rotate)
self.segment_mapper = dot_muncher
self._gpio = gpio or self.__rpi_gpio__()
self._WR = self._configure(WR)
self._DAT = self._configure(DAT)
self._CS = self._configure(CS)
self.command(0x30) # Internal RC oscillator @ 256KHz
self.command(0x52) # 1/2 Bias and 4 commons
self.command(0x02) # System enable
self.clear()
self.show()
def _configure(self, pin):
if pin is not None:
self._gpio.setup(pin, self._gpio.OUT)
return pin
[docs]
def display(self, image):
"""
Takes a 1-bit :py:mod:`PIL.Image` and dumps it to the PCD8544
LCD display.
"""
assert image.mode == self.mode
assert image.size == self.size
image = self.preprocess(image)
buf = []
for x in range(self._w):
byte = 0
for y in range(self._h):
if image.getpixel((x, y)) > 0:
byte |= 1 << y
buf.append(byte)
self.data(buf)
[docs]
def command(self, cmd):
gpio = self._gpio
gpio.output(self._CS, gpio.LOW)
self._write_bits(0x80, 4) # Command mode
self._write_bits(cmd, 8)
gpio.output(self._CS, gpio.HIGH)
[docs]
def data(self, data):
gpio = self._gpio
gpio.output(self._CS, gpio.LOW)
self._write_bits(0xA0, 3) # Write mode
self._write_bits(0x00, 6) # Address
for byte in data:
self._write_bits(byte, 8)
gpio.output(self._CS, gpio.HIGH)
def _write_bits(self, value, num_bits):
gpio = self._gpio
for _ in range(num_bits):
gpio.output(self._WR, gpio.LOW)
bit = gpio.HIGH if value & 0x80 > 0 else gpio.LOW
gpio.output(self._DAT, bit)
value <<= 1
gpio.output(self._WR, gpio.HIGH)
[docs]
def cleanup(self):
"""
Attempt to reset the device & switching it off prior to exiting the
python process.
"""
super(ht1621, self).cleanup()
self.command(0x00) # System disable
self._gpio.cleanup()
[docs]
class uc1701x(backlit_device):
"""
Serial interface to a monochrome UC1701X 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:: 0.5.0
"""
def __init__(self, serial_interface=None, rotate=0, **kwargs):
super(uc1701x, self).__init__(luma.lcd.const.uc1701x, serial_interface, **kwargs)
self.capabilities(128, 64, rotate)
self._pages = self._h // 8
self.command(0xE2) # System reset
self.command(0x2C) # Power: Boost ON
self.command(0x2E) # Power: Voltage Regulator ON
self.command(0x2F) # Power: Voltage Follower ON
self.command(0xF8, 0x00) # Booster ratio to 4x
self.command(0x23) # Set resistor ratio = 3
self.command(0xA2) # Bias 1/9
self.command(0xC0) # Set COM direction
self.command(0xA1) # Set SEG direction
self.command(0xAC) # Static indicator
self.command(0xA6) # Disable inverse
self.command(0xA5) # Display all points
self.command(0xA4) # Normal Display
self.contrast(0xB0)
self.clear()
self.show()
[docs]
def display(self, image):
"""
Takes a 1-bit :py:mod:`PIL.Image` and dumps it to the UC1701X
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, 0x04, 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))
[docs]
def contrast(self, value):
"""
Sets the LCD contrast
"""
assert 0 <= value <= 255
self.command(0x81, value >> 2)
[docs]
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
rendered to the display's screen using the display's built-in font.
:param serial_interface: The serial interface (usually a
:py:class:`luma.core.interface.serial.parallel` instance) to delegate
sending data and commands through.
:param width: The number of characters that can be displayed on a line.
:type width: int
:param height: The number of lines the display supports.
:type height: int
:param undefined: character to use if a requested character is not in the
font tables
:type undefined: str
:param selected_font: the font table appropriate for the model of display
you are using. The hd44780 normally comes in a version with font A00
(ENGLISH_JAPANESE) or A02 (ENGLISH_EUROPEAN). You can provide either
the name ('A00' or 'A02') or the number (0 for 'A00', 1 for 'A02') for
the font your display contains.
:type selected_font: int or str
:param exec_time: Time in seconds to wait for a command to complete.
Default is 50 μs (1e-6 * 50) which typically is long enough for commands
to finish. If your display is not working correctly, you may want to
try increasing the exec_time delay.
:type exec_time: float
:param gpio_LIGHT: The GPIO pin to use for the backlight if it is controlled by
one of the GPIO pins.
:type gpio_LIGHT: int
:param active_low: Set to true if backlight is active low (default), false
otherwise.
:type active_low: bool
:param pwm_frequency: Use PWM for backlight brightness control with the
specified frequency when provided.
:type pwm_frequency: float
:type backpack_pin: If using an I2C backpack, sets the pin on the backpack that
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 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::
p = parallel(RS=7, E=8, PINS=[25,24,23,18])
my_display = hd44780(p, selected_font='A00')
my_display.text = 'HD44780 Display\\nFont A00 Eng/Jap'
For more details on how to use the 'text' interface see
:class:`luma.core.virtual.character`
..note:
This driver currently only supports the hd44780 5x8 display mode.
.. versionadded:: 2.5.0
"""
def __init__(self, serial_interface=None, width=16, height=2, undefined='_',
selected_font=0, exec_time=0.000001, framebuffer=None, **kwargs):
super(hd44780, self).__init__(luma.lcd.const.hd44780, serial_interface,
exec_time=exec_time, **kwargs)
# Inherited from parallel_device class but multi-inheritence with
# backlit_device requires it to be initialized here
self._exec_time = exec_time
self.capabilities(width * 5, height * 8, 0)
self.init_framebuffer(framebuffer, 1)
# Currently only support 5x8 fonts for the hd44780
self.font = embedded_fonts(self._const.FONTDATA,
selected_font=selected_font)
self.glyph_index = self.font.current.glyph_index
self.device = self
self._undefined = undefined
self._custom = {}
# Supported modes
supported = (width, height) in [
(16, 2),
(16, 4),
(20, 2),
(20, 4)
]
if not supported:
raise luma.core.error.DeviceDisplayModeError(
f"Unsupported display mode: {width} x {height}")
self._initialize_device()
self.text = ''
self.command(self._const.CLEAR, exec_time=1e-3 * 1.5)
def _initialize_device(self):
"""
HD44780 Initialization Routine
Command Values
FUNCTIONSET = 0x20
DL8 = 0x10
DL4 = 0x00
LINES2 = 0x08
LINES1 = 0x00
CHAR5x8 = 0x00
CHAR5x10 = 0x04
DISPLAYOFF = 0x08
DISPLAYON = 0x0C
CLEAR = 0x01
ENTRY = 0x06
8 bit mode
Action Delay
---------------------------------------------------------------- -------
PowerOn 40.0ms
Command 0x30 4.1ms
Command 0x30 100μs
Command 0x30 40μs
Command FUNCTIONSET|DL8|(LINES2 or LINES1)|(CHAR5x8 or CHAR5x10) 40μs
Command DISPLAYOFF 40μs
Command CLEAR 40μs
Command ENTRY ; No shift, incrementing display 40μs
4bit mode
Action Delay
---------------------------------------------------------------- -------
PowerOn 40.0ms
Command 0x03 ; 4 bit write 4.1ms
Command 0x03 ; 4 bit write 100μs
Command 0x32 40μs
Command FUNCTIONSET|DL4|(LINES2 or LINES1)|(CHAR5x8 or CHAR5x10) 40μs
Command DISPLAYOFF 40μs
Command CLEAR 40μs
Command ENTRY ; No shift, incrementing display 40μs
Device is now ready to receive data
"""
sleep(1e-3 * 100)
if self._bitmode == 8:
self.command(0x30, exec_time=1e-3 * 10)
self.command(0x30, exec_time=1e-6 * 200)
self.command(0x30)
else:
# In case of stubborn display place in 8 bit mode three times and
# and then try to get it into four bit mode
for _ in range(3):
self.command(0x03, exec_time=1e-3 * 10, only_low_bits=True)
self.command(0x03, exec_time=1e-3 * 10, only_low_bits=True)
self.command(0x33, exec_time=1e-3 * 100)
self.command(0x03, exec_time=1e-3 * 10, only_low_bits=True)
self.command(0x03, exec_time=1e-6 * 200, only_low_bits=True)
self.command(0x32)
dl = self._const.DL8 if self._bitmode == 8 else self._const.DL4
self.command(self._const.FUNCTIONSET | dl | self._const.LINES2)
self.command(self._const.DISPLAYOFF) # Set Display Off
self.command(self._const.ENTRY) # Set entry mode to right, no shift
self.command(self._const.DISPLAYON, exec_time=1e-3 * 100) # Turn display on
[docs]
def display(self, image):
"""
Takes a 1-bit :py:mod:`PIL.Image` and converts it to text data
by reversing from glyphs from the image back to the correct
value from the displays font table.
:param image: the image to place on the display
:type image: :py:class:`PIL.Image.Image`
If needed, it will create custom characters if a glyph is not found
within the font table.
.. note:
Most hd44780s have limited memory to support custom characters
and typically can only support 8 at any one time. If this is
exceeded, the remaining unmatched characters will be replaced by
the ``undefined`` character.
"""
assert image.mode == self.mode
assert image.size == self.size
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 = 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
for j in range(top // 8, bottom // 8):
buf = []
for i in range(left // 5, right // 5):
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
if c is None:
self._make_custom(img)
c = self._custom.get(bytes, ord(self._undefined))
buf.append(c)
self.command(self._const.DDRAMADDR | (self._const.LINES[j] + (left // 5)))
self.data(buf)
def _make_custom(self, img):
"""
Create a new custom character based upon the provided image
.. note:
The image must be the same size as the font mode of the display.
"""
assert img.size == (5, 8)
if len(self._custom) == self._const.CUSTOMCHARS:
# Max custom characters already reached
return
idx = 0
for _ in range(self._const.CUSTOMCHARS):
if idx not in self._custom.values():
break
idx += 1
assert idx < self._const.CUSTOMCHARS
self.command(self._const.CGRAMADDR + (idx * 8))
data = [int(bool(i)) for i in img.getdata()]
buf = []
for j in range(8):
buf.append(sum(v << (4 - i) for i, v in
enumerate(data[j * 5:(j + 1) * 5])))
self.data(buf)
self._custom[img.tobytes()] = idx
def _cleanup_custom(self, img):
"""
Look across new image and remove any custom characters that are not
needed to render the image
"""
# If no custom characters then we can exit without the search
if len(self._custom) == 0:
return
in_use = []
for j in range(img.size[1] // 8):
for i in range(img.size[0] // 5):
data = img.crop((i * 5, j * 8, (i + 1) * 5, (j + 1) * 8)).tobytes()
if data in self._custom:
in_use.append(data)
self._custom = {k: v for k, v in self._custom.items() if k in in_use}
[docs]
def get_font(self, ft):
"""
Return one of the devices built-in fonts as a :py:mod:`PIL.ImageFont`
object
:param ft: name or number of the font to return (0 - A00, 1 - A02)
:type ft: int or str
"""
return self.font.load(ft)
[docs]
def contrast(self, *args):
"""
Not support on this device. Ignore.
"""
pass