diff --git a/CHANGES.rst b/CHANGES.rst index bd60d14b..86b730be 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,8 @@ ChangeLog +------------+---------------------------------------------------------------------+------------+ | Version | Description | Date | +============+=====================================================================+============+ +| *upcoming* | * Add support for 128x64 monochrome OLED (SSD1309) | | ++------------+---------------------------------------------------------------------+------------+ | **3.0.1** | * Fix bug where SSD1325/1327 didn't handle ``framebuffer`` properly | | +------------+---------------------------------------------------------------------+------------+ | **3.0.0** | * **BREAKING** Fix SSD1351 init sequence didn't set RGB/BGR color | 2018/12/02 | diff --git a/README.rst b/README.rst index c9c3b895..31eae86a 100644 --- a/README.rst +++ b/README.rst @@ -8,7 +8,7 @@ luma.oled Luma.OLED --------- -**Display drivers for SSD1306 / SSD1322 / SSD1325 / SSD1327 / SSD1331 / SSD1351 / SH1106** +**Display drivers for SSD1306 / SSD1309 / SSD1322 / SSD1325 / SSD1327 / SSD1331 / SSD1351 / SH1106** .. image:: https://travis-ci.org/rm-hull/luma.oled.svg?branch=master :target: https://travis-ci.org/rm-hull/luma.oled @@ -27,9 +27,9 @@ Luma.OLED .. image:: https://img.shields.io/maintenance/yes/2018.svg?maxAge=2592000 -Python library interfacing OLED matrix displays with the SSD1306, SSD1322, -SSD1325, SSD1327, SSD1331, SSD1351 or SH1106 driver using I2C/SPI on the -Raspberry Pi and other linux-based single-board computers - it provides a +Python library interfacing OLED matrix displays with the SSD1306, SSD1309, +SSD1322, SSD1325, SSD1327, SSD1331, SSD1351 or SH1106 driver using I2C/SPI on +the Raspberry Pi and other linux-based single-board computers - it provides a Pillow-compatible drawing canvas, and other functionality to support: * scrolling/panning capability, diff --git a/doc/index.rst b/doc/index.rst index 4c768b73..eeeb5fac 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -1,5 +1,5 @@ -Luma.OLED: Display drivers for SSD1306 / SSD1322 / SSD1325 / SSD1327 / SSD1331 / SSD1351 / SH1106 -================================================================================================= +Luma.OLED: Display drivers for SSD1306 / SSD1309 / SSD1322 / SSD1325 / SSD1327 / SSD1331 / SSD1351 / SH1106 +=========================================================================================================== .. image:: https://travis-ci.org/rm-hull/luma.oled.svg?branch=master :target: https://travis-ci.org/rm-hull/luma.oled diff --git a/doc/intro.rst b/doc/intro.rst index 379712c0..29c1f7d1 100644 --- a/doc/intro.rst +++ b/doc/intro.rst @@ -2,10 +2,10 @@ Introduction ------------ Interfacing `OLED matrix displays `_ with the -SSD1306, SSD1322, SSD1325, SSD1327, SSD1331, SSD1351 or SH1106 driver in Python -2 or 3 using I2C/SPI on the Raspberry Pi and other linux-based single-board -computers: the library provides a Pillow-compatible drawing canvas, and other -functionality to support: +SSD1306, SSD1309, SSD1322, SSD1325, SSD1327, SSD1331, SSD1351 or SH1106 driver +in Python 2 or 3 using I2C/SPI on the Raspberry Pi and other linux-based +single-board computers: the library provides a Pillow-compatible drawing +canvas, and other functionality to support: * scrolling/panning capability, * terminal-style printing, @@ -27,6 +27,7 @@ and will fit neatly inside the RPi case. in the following datasheets: - :download:`SSD1306 ` + - :download:`SSD1309 ` - :download:`SSD1322 ` - :download:`SSD1325 ` - :download:`SSD1327 ` diff --git a/doc/python-usage.rst b/doc/python-usage.rst index a8441ea2..c1626cf6 100644 --- a/doc/python-usage.rst +++ b/doc/python-usage.rst @@ -11,7 +11,7 @@ First, import and initialise the device: from luma.core.interface.serial import i2c, spi from luma.core.render import canvas - from luma.oled.device import ssd1306, ssd1325, ssd1331, sh1106 + from luma.oled.device import ssd1306, ssd1309, ssd1325, ssd1331, sh1106 # rev.1 users set port=0 # substitute spi(device=0, port=0) below if using that interface diff --git a/doc/tech-spec/SSD1309.pdf b/doc/tech-spec/SSD1309.pdf new file mode 100644 index 00000000..5ee03278 Binary files /dev/null and b/doc/tech-spec/SSD1309.pdf differ diff --git a/luma/oled/__init__.py b/luma/oled/__init__.py index e7fe86b7..a0ea4d8a 100644 --- a/luma/oled/__init__.py +++ b/luma/oled/__init__.py @@ -3,8 +3,8 @@ # See LICENSE.rst for details. """ -OLED display driver for SSD1306, SSD1322, SSD1325, SSD1327, SSD1331, SSD1351 -and SH1106 devices. +OLED display driver for SSD1306, SSD1309, SSD1322, SSD1325, SSD1327, SSD1331, +SSD1351 and SH1106 devices. """ __version__ = '3.0.1' diff --git a/luma/oled/device/__init__.py b/luma/oled/device/__init__.py index ede442b6..947216f4 100644 --- a/luma/oled/device/__init__.py +++ b/luma/oled/device/__init__.py @@ -42,7 +42,7 @@ import luma.oled.const -__all__ = ["ssd1306", "ssd1322", "ssd1325", "ssd1327", "ssd1331", "ssd1351", "sh1106"] +__all__ = ["ssd1306", "ssd1309", "ssd1322", "ssd1325", "ssd1327", "ssd1331", "ssd1351", "sh1106"] class sh1106(device): @@ -129,6 +129,18 @@ class ssd1306(device): 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) @@ -175,8 +187,8 @@ def __init__(self, serial_interface=None, width=128, height=64, rotate=0, **kwar def display(self, image): """ - Takes a 1-bit :py:mod:`PIL.Image` and dumps it to the SSD1306 - OLED display. + Takes a 1-bit :py:mod:`PIL.Image` and dumps it to the OLED + display. """ assert(image.mode == self.mode) assert(image.size == self.size) @@ -202,6 +214,30 @@ def display(self, image): self.data(list(buf)) +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 + """ + + class ssd1331(color_device): """ Serial interface to a 16-bit color (5-6-5 RGB) SSD1331 OLED display. @@ -211,7 +247,7 @@ class ssd1331(color_device): 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 + :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 @@ -282,7 +318,7 @@ class ssd1351(color_device): 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 + :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 @@ -382,7 +418,7 @@ class ssd1322(greyscale_device): 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 + :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 diff --git a/setup.py b/setup.py index 06750dc1..7dde04f2 100644 --- a/setup.py +++ b/setup.py @@ -45,12 +45,13 @@ def find_version(*file_paths): author="Richard Hull", author_email="richard.hull@destructuring-bind.org", description=("A small library to drive an OLED device with either " - "SSD1306, SSD1322, SSD1325, SSD1327, SSD1331, SSD1351 or SH1106 chipset"), + "SSD1306, SSD1309, SSD1322, SSD1325, SSD1327, SSD1331, " + "SSD1351 or SH1106 chipset"), long_description="\n\n".join([README, CONTRIB, CHANGES]), license="MIT", keywords=("raspberry pi rpi oled display screen " "rgb monochrome greyscale color " - "ssd1306 ssd1322 ssd1325 ssd1327 ssd1331 ssd1351 sh1106 " + "ssd1306 ssd1309 ssd1322 ssd1325 ssd1327 ssd1331 ssd1351 sh1106 " "spi i2c 256x64 128x64 128x32 96x16"), url="https://github.com/rm-hull/luma.oled", download_url="https://github.com/rm-hull/luma.oled/tarball/" + version, diff --git a/tests/reference/data/demo_ssd1309.json b/tests/reference/data/demo_ssd1309.json new file mode 100644 index 00000000..32c27302 --- /dev/null +++ b/tests/reference/data/demo_ssd1309.json @@ -0,0 +1 @@ +[255, 1, 1, 1, 1, 1, 129, 97, 25, 9, 5, 5, 5, 5, 5, 9, 25, 97, 129, 1, 1, 1, 1, 1, 253, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 253, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 225, 29, 225, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 13, 113, 129, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 129, 113, 13, 1, 225, 225, 129, 225, 225, 33, 129, 193, 65, 193, 129, 1, 17, 17, 241, 241, 1, 1, 17, 17, 241, 241, 1, 1, 129, 193, 65, 193, 129, 1, 1, 1, 1, 1, 1, 1, 1, 255, 255, 0, 0, 0, 240, 14, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 14, 240, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 248, 7, 0, 7, 248, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 28, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 28, 3, 0, 0, 0, 7, 7, 0, 7, 7, 4, 3, 7, 5, 5, 5, 0, 4, 4, 7, 7, 4, 4, 4, 4, 7, 7, 4, 4, 3, 7, 4, 7, 3, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 192, 63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 63, 192, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 128, 126, 1, 0, 0, 0, 1, 126, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 56, 192, 0, 0, 0, 0, 0, 192, 56, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 224, 31, 0, 0, 0, 0, 0, 0, 0, 31, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 14, 112, 128, 112, 14, 1, 0, 0, 0, 0, 0, 0, 0, 0, 30, 112, 62, 112, 30, 2, 56, 124, 68, 124, 56, 0, 68, 124, 120, 76, 4, 12, 65, 65, 127, 127, 64, 64, 56, 124, 69, 127, 127, 64, 0, 94, 94, 0, 0, 0, 0, 255, 255, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 0, 0, 248, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 248, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 112, 14, 1, 14, 112, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 1, 126, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 126, 1, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 128, 126, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 126, 128, 0, 0, 0, 0, 0, 0, 0, 0, 224, 28, 3, 0, 0, 0, 0, 0, 3, 28, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 7, 56, 192, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 56, 7, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 224, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 31, 224, 0, 0, 0, 0, 192, 56, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 56, 192, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 128, 128, 128, 128, 128, 128, 131, 140, 136, 144, 144, 144, 144, 144, 136, 140, 131, 128, 128, 128, 128, 128, 128, 191, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 191, 128, 184, 167, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 167, 184, 128, 176, 142, 129, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 129, 142, 176, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 255] \ No newline at end of file diff --git a/tests/test_ssd1309.py b/tests/test_ssd1309.py new file mode 100644 index 00000000..6fc0bc38 --- /dev/null +++ b/tests/test_ssd1309.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (c) 2014-18 Richard Hull and contributors +# See LICENSE.rst for details. + +from luma.oled.device import ssd1309 +from luma.core.render import canvas + +from baseline_data import primitives, get_json_data +from helpers import serial, call, setup_function, assert_invalid_dimensions # noqa: F401 + + +def test_init_128x64(): + """ + SSD1309 OLED with a 128 x 64 resolution works correctly. + """ + ssd1309(serial) + serial.command.assert_has_calls([ + # Initial burst are initialization commands + call(174, 213, 128, 168, 63, 211, 0, 64, 141, 20, 32, 0, + 161, 200, 218, 18, 217, 241, 219, 64, 164, 166), + # set contrast + call(129, 207), + # reset the display + call(33, 0, 127, 34, 0, 7), + # called last, is a command to show the screen + call(175) + ]) + + # Next are all data: zero's to clear the RAM + serial.data.assert_called_once_with([0] * (128 * 64 // 8)) + + +def test_init_invalid_dimensions(): + """ + SSD1309 OLED with an invalid resolution raises a + :py:class:`luma.core.error.DeviceDisplayModeError`. + """ + assert_invalid_dimensions(ssd1309, serial, 59, 22) + + +def test_hide(): + """ + SSD1309 OLED screen content can be hidden. + """ + device = ssd1309(serial) + serial.reset_mock() + device.hide() + serial.command.assert_called_once_with(174) + + +def test_show(): + """ + SSD1309 OLED screen content can be displayed. + """ + device = ssd1309(serial) + serial.reset_mock() + device.show() + serial.command.assert_called_once_with(175) + + +def test_display(): + """ + SSD1309 OLED screen can draw and display an image. + """ + device = ssd1309(serial) + serial.reset_mock() + + # Use the same drawing primitives as the demo + with canvas(device) as draw: + primitives(device, draw) + + # Initial command to reset the display + serial.command.assert_called_once_with(33, 0, 127, 34, 0, 7) + + # Next 1024 bytes are data representing the drawn image + serial.data.assert_called_once_with(get_json_data('demo_ssd1309'))