Source code for oled.device

# -*- coding: utf-8 -*-
# Copyright (c) 2016 Richard Hull and contributors
# See LICENSE.rst for details.

# Example usage:
#
#   from oled.serial import i2c, spi
#   from oled.device import ssd1306, sh1106
#   from oled.render import canvas
#   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

import atexit

from oled.serial import i2c
import oled.mixin as mixin
import oled.error
import oled.const


[docs]class device(mixin.capabilities): """ Base class for OLED driver classes .. warning:: Direct use of the :func:`command` and :func:`data` methods are discouraged: Screen updates should be effected through the :func:`display` method, or preferably with the :class:`oled.render.canvas` context manager. """ def __init__(self, const=None, serial_interface=None): self._const = const or oled.const.common self._serial_interface = serial_interface or i2c() atexit.register(self.cleanup)
[docs] def command(self, *cmd): """ Sends a command or sequence of commands through to the delegated serial interface. """ self._serial_interface.command(*cmd)
[docs] def data(self, data): """ Sends a data byte or sequence of data bytes through to the delegated serial interface. """ self._serial_interface.data(data)
[docs] def show(self): """ Sets the display mode ON, waking the device out of a prior low-power sleep mode. """ self.command(self._const.DISPLAYON)
[docs] def hide(self): """ Switches the display mode OFF, putting the device in low-power sleep mode. """ self.command(self._const.DISPLAYOFF)
[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(level >= 0) assert(level <= 255) self.command(self._const.SETCONTRAST, level)
[docs] def cleanup(self): self.hide() self.clear() self._serial_interface.cleanup()
[docs]class sh1106(device): """ Encapsulates the serial interface to the monochrome SH1106 OLED display hardware. 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): super(sh1106, self).__init__(oled.const.sh1106, serial_interface) self.capabilities(width, height, rotate) self._pages = self._h // 8 # FIXME: Delay doing anything here with alternate screen sizes # until we are able to get a device to test with. if width != 128 or height != 64: raise oled.error.DeviceDisplayModeError( "Unsupported display mode: {0} x {1}".format(width, 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, 0x3F, self._const.DISPLAYALLON_RESUME, self._const.SETDISPLAYOFFSET, 0x00, 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. """ 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 ssd1306(device): """ Encapsulates the serial interface to the monochrome SSD1306 OLED display hardware. 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): super(ssd1306, self).__init__(oled.const.ssd1306, serial_interface) self.capabilities(width, height, rotate) self._pages = self._h // 8 self._buffer = [0] * self._w * self._pages self._offsets = [n * self._w for n in range(8)] # Supported modes settings = { (128, 64): dict(multiplex=0x3F, displayclockdiv=0x80, compins=0x12), (128, 32): dict(multiplex=0x1F, displayclockdiv=0x80, compins=0x02), (96, 16): dict(multiplex=0x0F, displayclockdiv=0x60, compins=0x02) }.get((width, height)) if settings is None: raise oled.error.DeviceDisplayModeError( "Unsupported display mode: {0} x {1}".format(width, height)) 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.SETREMAP, 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 SSD1306 OLED display. """ assert(image.mode == self.mode) assert(image.size == self.size) image = self.preprocess(image) self.command( # Column start/end address self._const.COLUMNADDR, 0x00, self._w - 1, # Page start/end address self._const.PAGEADDR, 0x00, self._pages - 1) w = self._w pix = list(image.getdata()) step = w * 8 buf = self._buffer os0, os1, os2, os3, os4, os5, os6, os7 = self._offsets j = 0 for y in range(0, self._pages * step, step): i = y + w - 1 while i >= y: buf[j] = \ (0x01 if pix[i] > 0 else 0) | \ (0x02 if pix[i + os1] > 0 else 0) | \ (0x04 if pix[i + os2] > 0 else 0) | \ (0x08 if pix[i + os3] > 0 else 0) | \ (0x10 if pix[i + os4] > 0 else 0) | \ (0x20 if pix[i + os5] > 0 else 0) | \ (0x40 if pix[i + os6] > 0 else 0) | \ (0x80 if pix[i + os7] > 0 else 0) i -= 1 j += 1 self.data(buf)
[docs]class ssd1331(device): """ Encapsulates the serial interface to the 16-bit color (5-6-5 RGB) SSD1331 OLED display hardware. 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=96, height=64, rotate=0): super(ssd1331, self).__init__(oled.const.ssd1331, serial_interface) self.capabilities(width, height, rotate, mode="RGB") self._buffer = [0] * self._w * self._h * 2 if width != 96 or height != 64: raise oled.error.DeviceDisplayModeError( "Unsupported display mode: {0} x {1}".format(width, height)) self.command( self._const.DISPLAYOFF, self._const.SETREMAP, 0x72, self._const.SETDISPLAYSTARTLINE, 0x00, self._const.SETDISPLAYOFFSET, 0x00, self._const.NORMALDISPLAY, self._const.SETMULTIPLEX, 0x3F, self._const.SETMASTERCONFIGURE, 0x8E, self._const.POWERSAVEMODE, 0x0B, self._const.PHASE12PERIOD, 0x74, self._const.CLOCKDIVIDER, 0xD0, self._const.SETPRECHARGESPEEDA, 0x80, self._const.SETPRECHARGESPEEDB, 0x80, self._const.SETPRECHARGESPEEDC, 0x80, self._const.SETPRECHARGEVOLTAGE, 0x3E, self._const.SETVVOLTAGE, 0x3E, self._const.MASTERCURRENTCONTROL, 0x0F) self.contrast(0xFF) self.clear() self.show()
[docs] def display(self, image): """ Takes a 24-bit RGB :py:mod:`PIL.Image` and dumps it to the SSD1331 OLED display. """ assert(image.mode == self.mode) assert(image.size == self.size) image = self.preprocess(image) self.command( self._const.SETCOLUMNADDR, 0x00, self._w - 1, self._const.SETROWADDR, 0x00, self._h - 1) i = 0 buf = self._buffer for r, g, b in image.getdata(): # 65K format 1 buf[i] = r & 0xF8 | g >> 5 buf[i + 1] = g << 5 & 0xE0 | b >> 3 i += 2 self.data(buf)
[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(level >= 0) assert(level <= 255) self.command(self._const.SETCONTRASTA, level, self._const.SETCONTRASTB, level, self._const.SETCONTRASTC, level)
[docs]class ssd1325(device): """ Encapsulates the serial interface to the 4-bit greyscale SSD1325 OLED display hardware. 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): super(ssd1325, self).__init__(oled.const.ssd1325, serial_interface) self.capabilities(width, height, rotate, mode="RGB") self._buffer = [0] * (self._w * self._h // 2) if width != 128 or height != 64: raise oled.error.DeviceDisplayModeError( "Unsupported display mode: {0} x {1}".format(width, height)) self.command( self._const.DISPLAYOFF, self._const.SETCLOCK, 0xF1, self._const.SETMULTIPLEX, 0x3F, self._const.SETOFFSET, 0x4C, self._const.SETSTARTLINE, 0x00, self._const.MASTERCONFIG, 0x02, self._const.SETREMAP, 0x50, self._const.SETCURRENT + 2, self._const.SETGRAYTABLE, 0x01, 0x11, 0x22, 0x32, 0x43, 0x54, 0x65, 0x76) self.contrast(0xFF) self.command( self._const.SETROWPERIOD, 0x51, self._const.SETPHASELEN, 0x55, self._const.SETPRECHARGECOMP, 0x02, self._const.SETPRECHARGECOMPENABLE, 0x28, self._const.SETVCOMLEVEL, 0x1C, self._const.SETVSL, 0x0F, self._const.NORMALDISPLAY) self.clear() self.show()
[docs] def display(self, image): """ Takes a 24-bit RGB :py:mod:`PIL.Image` and dumps it to the SSD1325 OLED display, converting the image pixels to 4-bit greyscale using a simplified Luma calculation, based on *Y'=0.299R'+0.587G'+0.114B'*. """ assert(image.mode == self.mode) assert(image.size == self.size) image = self.preprocess(image) self.command( self._const.SETCOLUMNADDR, 0x00, self._w - 1, self._const.SETROWADDR, 0x00, self._h - 1) i = 0 buf = self._buffer for r, g, b in image.getdata(): # RGB->Greyscale luma calculation into 4-bits grey = (r * 306 + g * 601 + b * 117) >> 14 if i % 2 == 0: buf[i // 2] = grey else: buf[i // 2] |= (grey << 4) i += 1 self.data(buf)