# -*- coding: utf-8 -*-
# Copyright (c) 2014-2023 Richard Hull and contributors
# See LICENSE.rst for details.
"""
Collection of serial interfaces to OLED devices.
"""
# Example usage:
#
# from luma.core.interface.serial import i2c
# from luma.core.render import canvas
# from luma.oled.device import ssd1306
# from PIL import ImageDraw
#
# serial = i2c(port=1, address=0x3C)
# device = ssd1306(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.device import device, parallel_device
from luma.core.virtual import character
from luma.oled.device.color import color_device
from luma.oled.device.greyscale import greyscale_device
import luma.core.error
from luma.core.framebuffer import full_frame
from luma.core.bitmap_font import embedded_fonts
import luma.oled.const
from luma.oled.device.framebuffer_mixin import __framebuffer_mixin
__all__ = [
"ssd1306", "ssd1309", "ssd1322", "ssd1362", "ssd1322_nhd", "ssd1325",
"ssd1327", "ssd1331", "ssd1351", "sh1106", "sh1107", "ws0010",
"winstar_weh"
]
[docs]
class sh1106(device):
"""
Serial interface to a monochrome SH1106 OLED 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.
"""
def __init__(self, serial_interface=None, width=128, height=64, rotate=0, **kwargs):
super(sh1106, self).__init__(luma.oled.const.sh1106, serial_interface)
self.capabilities(width, height, rotate)
self._pages = self._h // 8
settings = {
(128, 128): dict(multiplex=0xFF, displayoffset=0x02),
(128, 64): dict(multiplex=0x3F, displayoffset=0x00),
(128, 32): dict(multiplex=0x20, displayoffset=0x0F)
}.get((width, height))
if settings is None:
raise luma.core.error.DeviceDisplayModeError(
f"Unsupported display mode: {width} x {height}")
self.command(
self._const.DISPLAYOFF,
self._const.MEMORYMODE,
self._const.SETHIGHCOLUMN, 0xB0, 0xC8,
self._const.SETLOWCOLUMN, 0x10, 0x40,
self._const.SETSEGMENTREMAP,
self._const.NORMALDISPLAY,
self._const.SETMULTIPLEX, settings['multiplex'],
self._const.DISPLAYALLON_RESUME,
self._const.SETDISPLAYOFFSET, settings['displayoffset'],
self._const.SETDISPLAYCLOCKDIV, 0xF0,
self._const.SETPRECHARGE, 0x22,
self._const.SETCOMPINS, 0x12,
self._const.SETVCOMDETECT, 0x20,
self._const.CHARGEPUMP, 0x14)
self.contrast(0x7F)
self.clear()
self.show()
[docs]
def display(self, image):
"""
Takes a 1-bit :py:mod:`PIL.Image` and dumps it to the SH1106
OLED display.
:param image: Image to display.
:type image: :py:mod:`PIL.Image`
"""
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, 0x02, 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]
class sh1107(device):
"""
Serial interface to a monochrome SH1107 OLED 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.i2c` instance) to delegate sending
data and commands through.
:param width: The number of horizontal pixels (optional, defaults to 64).
:type width: int
:param height: The number of vertical pixels (optional, defaults to 128).
: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
.. versionadded:: 3.11.0
"""
def __init__(self, serial_interface=None, width=64, height=128, rotate=0, **kwargs):
super(sh1107, self).__init__(luma.oled.const.sh1107, serial_interface)
self.capabilities(width, height, rotate)
self._pages = self._h // 8
self._pagelen = self._w
settings = {
(64, 128): dict(multiplex=0x7F, displayoffset=0x60),
(80, 128): dict(multiplex=0x4F, displayoffset=0x68),
(128, 128): dict(multiplex=0x7F, displayoffset=0x00)
}.get((width, height))
if settings is None:
raise luma.core.error.DeviceDisplayModeError(
f"Unsupported display mode: {width} x {height}")
self.command(
self._const.DISPLAYOFF,
self._const.MEMORYMODE,
self._const.NORMALDISPLAY,
self._const.SETMULTIPLEX, settings['multiplex'],
self._const.DISPLAYALLON_RESUME,
self._const.SETDISPLAYOFFSET, settings['displayoffset'],
self._const.SETDISPLAYCLOCKDIV, 0x80,
self._const.SETPRECHARGE, 0x22,
self._const.SETCOMPINS, 0x12,
self._const.SETVCOMDETECT, 0x35,
)
self.contrast(0x7F)
self.clear()
self.show()
[docs]
def display(self, image):
"""
Takes a 1-bit :py:mod:`PIL.Image` and dumps it to the SH1107
OLED display.
:param image: Image to display.
:type image: :py:mod:`PIL.Image`
"""
assert image.mode == self.mode
assert image.size == self.size
image = self.preprocess(image)
pixmap = image.load()
buf = bytearray(self._pagelen)
for page in range(self._pages):
for x in range(self._pagelen):
tmp = 0
for y in range(8):
tmp |= (pixmap[x, y + 8 * page] & 1) << y
buf[x] = tmp
self.command(0x10, 0x00, 0xb0 | page)
self.data(list(buf))
[docs]
class ssd1306(device):
"""
Serial interface to a monochrome SSD1306 OLED 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.i2c` instance) to delegate sending
data and commands through.
:param width: The number of horizontal pixels (optional, defaults to 128).
:type width: int
:param height: The number of vertical pixels (optional, defaults to 64).
: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
"""
def __init__(self, serial_interface=None, width=128, height=64, rotate=0, **kwargs):
super(ssd1306, self).__init__(luma.oled.const.ssd1306, serial_interface)
self.capabilities(width, height, rotate)
# Supported modes
settings = {
(128, 64): dict(multiplex=0x3F, displayclockdiv=0x80, compins=0x12, colstart=0),
(128, 32): dict(multiplex=0x1F, displayclockdiv=0x80, compins=0x02, colstart=0),
(96, 16): dict(multiplex=0x0F, displayclockdiv=0x60, compins=0x02, colstart=0),
(64, 48): dict(multiplex=0x2F, displayclockdiv=0x80, compins=0x12, colstart=32),
(64, 32): dict(multiplex=0x1F, displayclockdiv=0x80, compins=0x12, colstart=32)
}.get((width, height))
if settings is None:
raise luma.core.error.DeviceDisplayModeError(
f"Unsupported display mode: {width} x {height}")
self._pages = height // 8
self._mask = [1 << (i // width) % 8 for i in range(width * height)]
self._offsets = [(width * (i // (width * 8))) + (i % width) for i in range(width * height)]
self._colstart = settings['colstart']
self._colend = self._colstart + self._w
self.command(
self._const.DISPLAYOFF,
self._const.SETDISPLAYCLOCKDIV, settings['displayclockdiv'],
self._const.SETMULTIPLEX, settings['multiplex'],
self._const.SETDISPLAYOFFSET, 0x00,
self._const.SETSTARTLINE,
self._const.CHARGEPUMP, 0x14,
self._const.MEMORYMODE, 0x00,
self._const.SETSEGMENTREMAP,
self._const.COMSCANDEC,
self._const.SETCOMPINS, settings['compins'],
self._const.SETPRECHARGE, 0xF1,
self._const.SETVCOMDETECT, 0x40,
self._const.DISPLAYALLON_RESUME,
self._const.NORMALDISPLAY)
self.contrast(0xCF)
self.clear()
self.show()
[docs]
def display(self, image):
"""
Takes a 1-bit :py:mod:`PIL.Image` and dumps it to the OLED
display.
:param image: Image to display.
:type image: :py:mod:`PIL.Image`
"""
assert image.mode == self.mode
assert image.size == self.size
image = self.preprocess(image)
self.command(
# Column start/end address
self._const.COLUMNADDR, self._colstart, self._colend - 1,
# Page start/end address
self._const.PAGEADDR, 0x00, self._pages - 1)
buf = bytearray(self._w * self._pages)
off = self._offsets
mask = self._mask
idx = 0
for pix in image.getdata():
if pix > 0:
buf[off[idx]] |= mask[idx]
idx += 1
self.data(list(buf))
[docs]
class ssd1309(ssd1306):
"""
Serial interface to a monochrome SSD1309 OLED 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 horizontal pixels (optional, defaults to 128).
:type width: int
:param height: The number of vertical pixels (optional, defaults to 64).
: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
.. versionadded:: 3.1.0
"""
[docs]
class ssd1331(color_device):
"""
Serial interface to a 16-bit color (5-6-5 RGB) SSD1331 OLED 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 horizontal pixels (optional, defaults to 96).
:type width: int
:param height: The number of vertical pixels (optional, defaults to 64).
: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: str
"""
def __init__(self, serial_interface=None, width=96, height=64, rotate=0,
framebuffer=None, **kwargs):
super(ssd1331, self).__init__(serial_interface, width, height, rotate,
framebuffer, **kwargs)
def _supported_dimensions(self):
return [(96, 64)]
def _init_sequence(self):
self.command(
0xAE, # Display off
0xA0, 0x72, # Seg remap
0xA1, 0x00, # Set Display start line
0xA2, 0x00, # Set display offset
0xA4, # Normal display
0xA8, 0x3F, # Set multiplex
0xAD, 0x8E, # Master configure
0xB0, 0x0B, # Power save mode
0xB1, 0x74, # Phase12 period
0xB3, 0xD0, # Clock divider
0x8A, 0x80, # Set precharge speed A
0x8B, 0x80, # Set precharge speed B
0x8C, 0x80, # Set precharge speed C
0xBB, 0x3E, # Set pre-charge voltage
0xBE, 0x3E, # Set voltage
0x87, 0x0F) # Master current control
def _set_position(self, top, right, bottom, left):
self.command(
0x15, left, right - 1, # Set column addr
0x75, top, bottom - 1) # Set row addr
[docs]
def contrast(self, level):
"""
Switches the display contrast to the desired level, in the range
0-255. Note that setting the level to a low (or zero) value will
not necessarily dim the display to nearly off. In other words,
this method is **NOT** suitable for fade-in/out animation.
:param level: Desired contrast level in the range of 0-255.
:type level: int
"""
assert 0 <= level <= 255
self.command(0x81, level, # Set contrast A
0x82, level, # Set contrast B
0x83, level) # Set contrast C
[docs]
class ssd1351(color_device):
"""
Serial interface to the 16-bit color (5-6-5 RGB) SSD1351 OLED 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 horizontal pixels (optional, defaults to 128).
:type width: int
:param height: The number of vertical pixels (optional, defaults to 128).
: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: str
: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 h_offset: int
.. versionadded:: 2.3.0
"""
def __init__(self, serial_interface=None, width=128, height=128, rotate=0,
framebuffer=None, h_offset=0, v_offset=0,
bgr=False, **kwargs):
# RGB or BGR order
self._color_order = 0x04 if bgr else 0x00
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
super(ssd1351, self).__init__(serial_interface, width, height, rotate, framebuffer, **kwargs)
def _supported_dimensions(self):
return [(96, 96), (128, 128), (128, 96)]
def _init_sequence(self):
self.command(0xFD, 0x12) # Unlock IC MCU interface
self.command(0xFD, 0xB1) # Command A2,B1,B3,BB,BE,C1 accessible if in unlock state
self.command(0xAE) # Display off
self.command(0xB3, 0xF1) # Clock divider
self.command(0xCA, 0x7F) # Mux ratio
self.command(0x15, 0x00, self.width - 1) # Set column address
self.command(0x75, 0x00, self.height - 1) # Set row address
self.command(0xA0, 0x70 | self._color_order) # Segment remapping
self.command(0xA1, 0x00) # Set Display start line
self.command(0xA2, 0x00) # Set display offset
self.command(0xB5, 0x00) # Set GPIO
self.command(0xAB, 0x01) # Function select (internal - diode drop)
self.command(0xB1, 0x32) # Precharge
self.command(0xB4, 0xA0, 0xB5, 0x55) # Set segment low voltage
self.command(0xBE, 0x05) # Set VcomH voltage
self.command(0xC7, 0x0F) # Contrast master
self.command(0xB6, 0x01) # Precharge2
self.command(0xA6) # Normal display
def _set_position(self, top, right, bottom, left):
self.command(0x15, left, right - 1) # Set column addr
self.command(0x75, top, bottom - 1) # Set row addr
self.command(0x5C) # Write RAM
[docs]
def contrast(self, level):
"""
Switches the display contrast to the desired level, in the range
0-255. Note that setting the level to a low (or zero) value will
not necessarily dim the display to nearly off. In other words,
this method is **NOT** suitable for fade-in/out animation.
:param level: Desired contrast level in the range of 0-255.
:type level: int
"""
assert 0 <= level <= 255
self.command(0xC1, level, level, level)
[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 ssd1322(greyscale_device):
"""
Serial interface to a 4-bit greyscale SSD1322 OLED 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 horizontal pixels (optional, defaults to 96).
:type width: int
:param height: The number of vertical pixels (optional, defaults to 64).
: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 mode: Supplying "1" or "RGB" effects a different rendering
mechanism, either to monochrome or 4-bit greyscale.
:type mode: str
:param framebuffer: Framebuffering strategy, currently instances of
``diff_to_previous`` or ``full_frame`` are only supported
:type framebuffer: str
"""
def __init__(self, serial_interface=None, width=256, height=64, rotate=0,
mode="RGB", framebuffer=None, **kwargs):
self._column_offset = (480 - width) // 2
super(ssd1322, self).__init__(luma.oled.const.ssd1322, serial_interface,
width, height, rotate, mode, framebuffer,
nibble_order=0, **kwargs)
def _supported_dimensions(self):
return [(256, 64), (256, 48), (256, 32),
(128, 64), (128, 48), (128, 32),
(64, 64), (64, 48), (64, 32)]
def _init_sequence(self):
self.command(0xFD, 0x12) # Unlock IC
self.command(0xA4) # Display off (all pixels off)
self.command(0xB3, 0xF2) # Display divide clockratio/freq
self.command(0xCA, 0x3F) # Set MUX ratio
self.command(0xA2, 0x00) # Display offset
self.command(0xA1, 0x00) # Display start Line
self.command(0xA0, 0x14, 0x11) # Set remap & dual COM Line
self.command(0xB5, 0x00) # Set GPIO (disabled)
self.command(0xAB, 0x01) # Function select (internal Vdd)
self.command(0xB4, 0xA0, 0xFD) # Display enhancement A (External VSL)
self.command(0xC7, 0x0F) # Master contrast (reset)
self.command(0xB9) # Set default greyscale table
self.command(0xB1, 0xF0) # Phase length
self.command(0xD1, 0x82, 0x20) # Display enhancement B (reset)
self.command(0xBB, 0x0D) # Pre-charge voltage
self.command(0xB6, 0x08) # 2nd precharge period
self.command(0xBE, 0x00) # Set VcomH
self.command(0xA6) # Normal display (reset)
self.command(0xA9) # Exit partial display
def _set_position(self, top, right, bottom, left):
width = right - left
pix_start = self._column_offset + left
coladdr_start = pix_start >> 2
coladdr_end = (pix_start + width >> 2) - 1
self.command(0x15, coladdr_start, coladdr_end) # set column addr
self.command(0x75, top, bottom - 1) # Reset row addr
self.command(0x5C) # Enable MCU to write data into RAM
[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 ssd1362(greyscale_device):
"""
Serial interface to a 4-bit greyscale SSD1362 OLED 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 horizontal pixels (optional, defaults to 96).
:type width: int
:param height: The number of vertical pixels (optional, defaults to 64).
: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 mode: Supplying "1" or "RGB" effects a different rendering
mechanism, either to monochrome or 4-bit greyscale.
:type mode: str
:param framebuffer: Framebuffering strategy, currently instances of
``diff_to_previous`` or ``full_frame`` are only supported
:type framebuffer: str
.. versionadded:: 3.4.0
"""
def __init__(self, serial_interface=None, width=256, height=64, rotate=0,
mode="RGB", framebuffer=None, **kwargs):
super(ssd1362, self).__init__(luma.oled.const.ssd1362, serial_interface,
width, height, rotate, mode, framebuffer,
nibble_order=1, **kwargs)
def _supported_dimensions(self):
return [(256, 64)]
def _init_sequence(self):
self.command(
0xAB, # Set Vdd Mode
0x01, # ELW2106AA VCI = 3.0V
0xAD, 0x9E, # Set IREF selection
0x15, 0x00, 0x7F, # Set column address
0x75, 0x00, 0x3F, # Set row address
0xA0, 0x43, # Set Re-map
0xA1, 0x00, # Set display start line
0xA2, 0x00, # Set display offset
0xA4, # Set display mode
0xA8, 0x3F, # Set multiplex ratio
0xB1, 0x11, # Set Phase1,2 length
0xB3, 0xF0, # Set display clock divide ratio
0xB9, # Grey scale table
0xBC, 0x04, # Set pre-charge voltage
0xBE, 0x05) # Set VCOMH deselect level, 0.82 * Vcc
def _set_position(self, top, right, bottom, left):
self.command(
0x15, left >> 1, (right - 1) >> 1, # set column addr
0x75, top, bottom - 1) # set row addr
[docs]
class ssd1322_nhd(greyscale_device):
"""Similar to ssd1322 but several options are hard coded: width, height and
frame buffer"""
def __init__(self, serial_interface=None, width=128, height=64, rotate=0,
mode="RGB", framebuffer=full_frame(), **kwargs):
super(ssd1322_nhd, self).__init__(luma.oled.const.ssd1322, serial_interface,
128, 64, rotate, mode, framebuffer,
nibble_order=0, **kwargs)
def _supported_dimensions(self):
return [(128, 64)]
def _init_sequence(self):
self.command(0xFD, 0x12) # Unlock IC
self.command(0xAE) # Display off
self.command(0xB3, 0x91) # Display divide clockratio/freq
self.command(0xCA, 0x3F) # Set MUX ratio
self.command(0xA2, 0x00) # Display offset
self.command(0xAB, 0x01) # Internal VDD
self.command(0xA0, 0x16, 0x11) # Set remap & dual COM Line
self.command(0xC7, 0x0F) # Master contrast (reset)
self.command(0xC1, 0x9F) # Set contrast current
self.command(0xB1, 0xF2) # Phase 1 and 2 lengths
self.command(0xBB, 0x1F) # Pre-charge voltage
self.command(0xB4, 0xA0, 0xFD) # Display enhancement A (External VSL)
self.command(0xBE, 0x04) # Set VcomH
self.command(0xA6) # Normal display (reset)
self.command(0xAF) # Exit partial display
def _set_position(self, top, bottom):
coladdr_start = 28
coladdr_end = 91
self.command(0x15, coladdr_start, coladdr_end) # set column addr
self.command(0x75, top, bottom - 1) # Reset row addr
self.command(0x5C) # Enable MCU to write data into RAM
[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))
def _render_mono(self, buf, pixel_data):
i = 0
for pix in pixel_data:
if pix > 0:
buf[i] = 0xFF
i += 1
def _render_greyscale(self, buf, pixel_data):
i = 0
for r, g, b in pixel_data:
# RGB->Greyscale luma calculation into 4-bits
grey = (r * 306 + g * 601 + b * 117) >> 14
# NHD uses 2 SEG lines and one COM line per pixel
if grey > 0:
buf[i // 2] |= (grey << 4) | grey
i += 2
[docs]
def display(self, image):
"""
Takes a 1-bit monochrome or 24-bit RGB image and renders it
to the greyscale OLED display. RGB pixels are converted to 8-bit
greyscale values using a simplified Luma calculation, based on
*Y'=0.299R'+0.587G'+0.114B'*.
: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 = bounding_box
width = right - left
height = bottom - top
buf = bytearray(width * height)
self._set_position(top, bottom)
self._populate(buf, image.getdata())
self.data(list(buf))
[docs]
class ssd1325(greyscale_device):
"""
Serial interface to a 4-bit greyscale SSD1325 OLED 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.
"""
def __init__(self, serial_interface=None, width=128, height=64, rotate=0,
mode="RGB", framebuffer=None, **kwargs):
super(ssd1325, self).__init__(luma.core.const.common, serial_interface,
width, height, rotate, mode, framebuffer,
nibble_order=1, **kwargs)
def _supported_dimensions(self):
return [(128, 64)]
def _init_sequence(self):
self.command(
0xAE, # Diplay off (all pixels off)
0xB3, 0xF2, # Display divide clockratio/freq
0xA8, 0x3F, # Set MUX ratio
0xA2, 0x4C, # Display offset
0xA1, 0x00, # Display start line
0xAD, 0x02, # Master configuration (external Vcc)
0xA0, 0x50, # Set remap (enable COM remap & split odd/even)
0x86, # Set current range (full)
0xB8, 0x01, 0x11, # Set greyscale table
0x22, 0x32, 0x43, # .. cont
0x54, 0x65, 0x76, # .. cont
0xB2, 0x51, # Set row period
0xB1, 0x55, # Set phase length
0xB4, 0x03, # Set pre-charge compensation level
0xB0, 0x28, # Set pre-charge compensation enable
0xBC, 0x01, # Pre-charge voltage
0xBE, 0x00, # Set VcomH
0xBF, 0x02, # Set VSL (not connected)
0xA4) # Normal dislay
def _set_position(self, top, right, bottom, left):
self.command(
0x15, left >> 1, (right - 1) >> 1, # set column addr
0x75, top, bottom - 1) # set row addr
[docs]
class ssd1327(greyscale_device):
"""
Serial interface to a 4-bit greyscale SSD1327 OLED 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.
.. versionadded:: 2.4.0
"""
def __init__(self, serial_interface=None, width=128, height=128, rotate=0,
mode="RGB", framebuffer=None, **kwargs):
super(ssd1327, self).__init__(luma.core.const.common, serial_interface,
width, height, rotate, mode, framebuffer,
nibble_order=1, **kwargs)
def _supported_dimensions(self):
return [(128, 128)]
def _init_sequence(self):
self.command(
0xAE, # Display off (all pixels off)
0xA0, 0x53, # Segment remap (com split, com remap, nibble remap, column remap)
0xA1, 0x00, # Display start line
0xA2, 0x00, # Display offset
0xA4, # regular display
0xA8, 0x7F) # set multiplex ratio: 127
self.command(
0xB8, 0x01, 0x11, # Set greyscale table
0x22, 0x32, 0x43, # .. cont
0x54, 0x65, 0x76) # .. cont
self.command(
0xB3, 0x00, # Front clock divider: 0, Fosc: 0
0xAB, 0x01, # Enable Internal Vdd
0xB1, 0xF1, # Set phase periods - 1: 1 clk, 2: 15 clks
0xBC, 0x08, # Pre-charge voltage: Vcomh
0xBE, 0x07, # COM deselect voltage level: 0.86 x Vcc
0xD5, 0x62, # Enable 2nd pre-charge
0xB6, 0x0F) # 2nd Pre-charge period: 15 clks
def _set_position(self, top, right, bottom, left):
self.command(
0x15, left >> 1, (right - 1) >> 1, # set column addr
0x75, top, bottom - 1) # set row addr
[docs]
class ws0010(parallel_device, character, __framebuffer_mixin):
"""
Serial interface to a monochrome Winstar WS0010 OLED display. This
interface will work with most ws0010 powered devices including the weg010016.
: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 pixels laid out horizontally.
:type width: int
:param height: The number of pixels laid out vertically.
:type height: int
:param undefined: The character to display if the font doesn't contain
the requested character
:type undefined: str
:param font: Allows you to override the internal font by passing in an alternate
:type font: :py:mod:`PIL.ImageFont`
:param selected_font: Select one of the ws0010's embedded font tables (see
note). Can be selected by number or name. Default is 'FT00'
(English Japanese).
: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 is enough for all ws0010 commands.
:type exec_time: float
: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 values of
``diff_to_previous`` or ``full_frame`` are only supported.
:type framebuffer: str
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 = ws0010(p, selected_font='FT01')
my_display.text = 'WS0010 Display\\nFont FT01 5x8'
For more details on how to use the 'text' interface see
:class:`luma.core.virtual.character`
.. note:
The ws0010 is a fully graphical device that also supports character-based
operations similar to LCD displays such as the hd44780. This driver
uses the graphics mode but includes functionality to allow the device
to act like a character-based one. To do this it includes four
embedded fonts which are available in two sizes (5x8 pixel and 5x10
pixel). You can select which font to use by providing the appropriate
name or number to selected_font during initialization. You can also use
any PIL.ImageFont object instead by providing it to the font parameter.
If you do, the internal fonts will be bypassed.
Available Internal Fonts
+--------+---------+----------------------+------+
| Number | Name | Font | Size |
+--------+---------+----------------------+------+
| 0 | FT00 | English Japanese | 5x8 |
+--------+---------+----------------------+------+
| 1 | FT01 | Western European I | 5x8 |
+--------+---------+----------------------+------+
| 2 | FT10 | English Russian | 5x8 |
+--------+---------+----------------------+------+
| 3 | FT11 | Western European II | 5x8 |
+--------+---------+----------------------+------+
| 4 | FT00_10 | English Japanese | 5x10 |
+--------+---------+----------------------+------+
| 5 | FT01_10 | Western European I | 5x10 |
+--------+---------+----------------------+------+
| 6 | FT10_10 | English Russian | 5x10 |
+--------+---------+----------------------+------+
| 7 | FT11_10 | Western European II | 5x10 |
+--------+---------+----------------------+------+
.. versionadded:: 3.6.0
"""
def __init__(self, serial_interface=None, width=100, height=16, undefined='_', font=None,
selected_font=0, exec_time=1e-6 * 50, rotate=0, framebuffer=None,
const=luma.oled.const.ws0010, **kwargs):
super(ws0010, self).__init__(const, serial_interface, exec_time=exec_time, **kwargs)
self.capabilities(width, height, rotate)
self.init_framebuffer(framebuffer)
self.font = font if font is not None else embedded_fonts(self._const.FONTDATA, selected_font=selected_font)
self._undefined = undefined
self.device = self
# Supported modes
supported = (width, height) in [(40, 8), (40, 16), (60, 8), (60, 16), (80, 8), (80, 16), (100, 8), (100, 16)]
if not supported:
raise luma.core.error.DeviceDisplayModeError(
f"Unsupported display mode: {width} x {height}")
# In case display just powered up, sleep to be sure it has finished
# its internal initialization
sleep(0.5)
self._reset()
self.text = ""
def _reset(self):
"""
WS0010 Initialization Routine
Reset (not really needed after power-on but does no harm and is useful
in case the display is already initialized)
Send five 0s in four bit mode (e.g. d4-d7 are the only pins that matter)
Set interface data length FUNCTIONSET|DL8 or FUNCTIONSET|DL4 (again in 4 bit mode)
Turn Display Off
Turn internal power off
Configure Entry Mode (Set Cursor to Right and Shift Off)
Set Graphics Mode and Internal Power On
Turn Display On (with Cursor Off and Blink Off)
Device is now ready to receive data
"""
dl = 0x03 if self._bitmode == 8 else 0x02
self.command(0x00, 0x00, dl, self._const.FUNCTIONSET | (dl << 4))
self.command(self._const.DISPLAYOFF) # Set Display Off
self.command(self._const.POWEROFF)
self.command(self._const.ENTRY) # Set entry mode to direction right, no shift
self.command(self._const.POWERON | self._const.GRAPHIC) # Turn internal power on and set into graphics mode
self.command(self._const.DISPLAYON) # Turn Display back on
[docs]
def display(self, image):
"""
Takes a 1-bit :py:mod:`PIL.Image` and dumps it to the ws0010
OLED display.
"""
assert image.mode == self.mode
assert image.size == self.size
image = self.preprocess(image)
for _, bounding_box in self.framebuffer.redraw(image):
# Expand bounding box to align to cell height boundary (8)
# TODO: Should consider whether this should be moved into framebuffer class
left, top, right, bottom = bounding_box
top = top // 8 * 8
bottom = bottom // 8 * 8 if not bottom % 8 else (bottom // 8 + 1) * 8
if self._bitmode == 4:
# If in 4 bit mode, issue reset to make sure we are in sync with the display
self._reset()
w = right - left
h = bottom - top
mask = [1 << (i // w) % 8 for i in range(w * h)]
off = [(w * (i // (w * 8))) + (i % w) for i in range(w * h)]
buf = bytearray(w * h // 8)
image_segment = image.crop((left, top, right, bottom))
for idx, pix in enumerate(image_segment.getdata()):
if pix > 0:
buf[off[idx]] |= mask[idx]
lines = int((bottom - top) / 8)
lineSize = int(len(buf) / lines)
for i in range(lines):
self.command(self._const.DDRAMADDR + left, self._const.CGRAMADDR + i + (top // 8)) # Set display to current line at the starting column to update
self.data(buf[lineSize * i:lineSize * (i + 1)]) # Send section of current line that needs to be changed
[docs]
def get_font(self, ft):
"""
Load one of the devices embedded fonts by its index value or name and
return it
:param val: The index or the name of the font to return
:type val: int or str
"""
return self.font.load(ft)
[docs]
class winstar_weh(ws0010):
"""
Serial interface to a monochrome Winstar WEH OLED display. This is the
character version of the display using the ws0010 controller. This class
provides the same ``text`` property as the ws0010 interface so you can set
the text value which will be rendered to the display's screen. This
interface uses a variant of the ws0010 controller's built-in font that is
designed to match the grid structure of the weh displays (see note below).
: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 single line.
Example: the weh001602a has a width of 16 characters.
:type width: int
:param height: The number of lines the display has. Example: the weh001602a
has a height of 2 lines.
:type height: int
:param undefined: The character to display if the font doesn't contain
the requested character
:type undefined: str
:param font: Allows you to override the internal font by passing in an alternate
:type font: :py:mod:`PIL.ImageFont`
:param default_table: Select one of the ws0010's four embedded font tables
(see :py:class:`ws0010` documentation)
:type default_table: int
:param embedded_font: Select the size of the embedded font to use. Allowed
sizes are 5x8 (default) and 5x10
:type embedded_font: str '5x8' or '5x10'
:param exec_time: Time in seconds to wait for a command to complete.
Default is 50 μs (1e-6 * 50)
:type exec_time: float
: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 values of
``diff_to_previous`` or ``full_frame`` are only supported.
:type framebuffer: str
.. note:
The WEH devices mimic character displays by having a small gap every fifth
horizontal pixel. So, while the device can be addressed as a graphical
display, the images that are shown will have a one pixel gap every 5th pixel.
For this reason, you should be careful when designing you screens to
account for this effect.
The included text functionality automatically takes care of this for you
if you are displaying character-based content.
.. versionadded:: 3.6.0
"""
def __init__(self, serial_interface=None, width=16, height=2, **kwargs):
super(winstar_weh, self).__init__(const=luma.oled.const.winstar_weh, serial_interface=serial_interface, width=width * 5, height=height * 8, xwidth=5, **kwargs)