From e847c76afc7d557b5918abbf18189a8d0028e836 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 24 Aug 2022 13:04:51 +0100 Subject: [PATCH 1/4] Driver for SSD1683-based Inky wHAT New display IDs supported: 17 - Black wHAT (SSD1683) 18 - Red wHAT (SSD1683) 19 - Yellow wHAT (SSD1683) New board type "whatssd1683" added to "auto" args. --- examples/what/dither-image-what-1683.py | 66 ++++++ library/inky/__init__.py | 1 + library/inky/auto.py | 10 +- library/inky/eeprom.py | 5 +- library/inky/inky_ssd1683.py | 284 ++++++++++++++++++++++++ library/inky/ssd1683.py | 34 +++ 6 files changed, 398 insertions(+), 2 deletions(-) create mode 100755 examples/what/dither-image-what-1683.py create mode 100644 library/inky/inky_ssd1683.py create mode 100644 library/inky/ssd1683.py diff --git a/examples/what/dither-image-what-1683.py b/examples/what/dither-image-what-1683.py new file mode 100755 index 00000000..7e364bde --- /dev/null +++ b/examples/what/dither-image-what-1683.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 + +import argparse +from PIL import Image +from inky import InkyWHAT_SSD1683 as InkyWHAT + +print("""Inky wHAT: Dither image + +Converts and displays dithered images on Inky wHAT. +""") + +# Command line arguments to set display type and colour, and enter your name + +parser = argparse.ArgumentParser() +parser.add_argument('--colour', '-c', type=str, required=True, choices=["red", "black", "yellow"], help="ePaper display colour") +parser.add_argument('--image', '-i', type=str, required=True, help="Input image to be converted/displayed") +args = parser.parse_args() + +colour = args.colour +img_file = args.image + +# Set up the inky wHAT display and border colour + +inky_display = InkyWHAT((400, 300), colour) +inky_display.set_border(inky_display.WHITE) + +# Open our image file that was passed in from the command line + +img = Image.open(img_file) + +# Get the width and height of the image + +w, h = img.size + +# Calculate the new height and width of the image + +h_new = 300 +w_new = int((float(w) / h) * h_new) +w_cropped = 400 + +# Resize the image with high-quality resampling + +img = img.resize((w_new, h_new), resample=Image.LANCZOS) + +# Calculate coordinates to crop image to 400 pixels wide + +x0 = (w_new - w_cropped) / 2 +x1 = x0 + w_cropped +y0 = 0 +y1 = h_new + +# Crop image + +img = img.crop((x0, y0, x1, y1)) + +# Convert the image to use a white / black / red colour palette + +pal_img = Image.new("P", (1, 1)) +pal_img.putpalette((255, 255, 255, 0, 0, 0, 255, 0, 0) + (0, 0, 0) * 252) + +img = img.convert("RGB").quantize(palette=pal_img) + +# Display the final image on Inky wHAT + +inky_display.set_image(img) +inky_display.show() diff --git a/library/inky/__init__.py b/library/inky/__init__.py index b15a66cf..9da3b7c2 100644 --- a/library/inky/__init__.py +++ b/library/inky/__init__.py @@ -6,6 +6,7 @@ from .what import InkyWHAT # noqa: F401 from .mock import InkyMockPHAT, InkyMockWHAT # noqa: F401 from .inky_uc8159 import Inky as Inky7Colour # noqa: F401 +from .inky_ssd1683 import Inky as InkyWHAT_SSD1683 # noqa: F401 from .auto import auto # noqa: F401 __version__ = '1.3.2' diff --git a/library/inky/auto.py b/library/inky/auto.py index e13bf4d7..6239595d 100644 --- a/library/inky/auto.py +++ b/library/inky/auto.py @@ -2,6 +2,7 @@ from .phat import InkyPHAT, InkyPHAT_SSD1608 # noqa: F401 from .what import InkyWHAT # noqa: F401 from .inky_uc8159 import Inky as InkyUC8159 # noqa: F401 +from .inky_ssd1683 import Inky as InkyWHAT_SSD1683 # noqa: F401 from . import eeprom import argparse @@ -24,13 +25,15 @@ def auto(i2c_bus=None, ask_user=False, verbose=False): return InkyUC8159(resolution=(600, 448)) if _eeprom.display_variant in (15, 16): return InkyUC8159(resolution=(640, 400)) + if _eeprom.display_variant in (17, 18, 19): + return InkyWHAT_SSD1683((400, 300), _eeprom.get_color()) if ask_user: if verbose: print("Failed to detect an Inky board. Trying --type/--colour arguments instead...\n") parser = argparse.ArgumentParser() parser.add_argument('--simulate', '-s', action='store_true', default=False, help="Simulate Inky display") - parser.add_argument('--type', '-t', type=str, required=True, choices=["what", "phat", "phatssd1608", "impressions", "7colour"], help="Type of display") + parser.add_argument('--type', '-t', type=str, required=True, choices=["what", "phat", "phatssd1608", "impressions", "7colour", "whatssd1683"], help="Type of display") parser.add_argument('--colour', '-c', type=str, required=False, choices=["red", "black", "yellow"], help="Display colour") args, _ = parser.parse_known_args() if args.simulate: @@ -47,6 +50,9 @@ def auto(i2c_bus=None, ask_user=False, verbose=False): if args.type in ("impressions", "7colour"): from .mock import InkyMockImpression cls = InkyMockImpression() + if args.type == "whatssd1683": + from .mock import InkyMockWHAT + cls = InkyMockWHAT(args.colour) if cls is not None: import atexit atexit.register(cls.wait_for_window_close) @@ -59,6 +65,8 @@ def auto(i2c_bus=None, ask_user=False, verbose=False): return InkyPHAT_SSD1608(args.colour) if args.type == "what": return InkyWHAT(args.colour) + if args.type == "whatssd1683": + return InkyWHAT_SSD1683(args.colour) if args.type in ("impressions", "7colour"): return InkyUC8159() diff --git a/library/inky/eeprom.py b/library/inky/eeprom.py index 70e5ea31..11422b56 100644 --- a/library/inky/eeprom.py +++ b/library/inky/eeprom.py @@ -27,7 +27,10 @@ None, '7-Colour (UC8159)', '7-Colour 640x400 (UC8159)', - '7-Colour 640x400 (UC8159)' + '7-Colour 640x400 (UC8159)', + 'Black wHAT (SSD1683)', + 'Red wHAT (SSD1683)', + 'Yellow wHAT (SSD1683)' ] diff --git a/library/inky/inky_ssd1683.py b/library/inky/inky_ssd1683.py new file mode 100644 index 00000000..41633095 --- /dev/null +++ b/library/inky/inky_ssd1683.py @@ -0,0 +1,284 @@ +"""Inky e-Ink Display Driver.""" +import time + +from PIL import Image +from . import eeprom, ssd1683 + +try: + import numpy +except ImportError: + raise ImportError('This library requires the numpy module\nInstall with: sudo apt install python-numpy') + +WHITE = 0 +BLACK = 1 +RED = YELLOW = 2 + +RESET_PIN = 27 +BUSY_PIN = 17 +DC_PIN = 22 + +MOSI_PIN = 10 +SCLK_PIN = 11 +CS0_PIN = 0 + +SUPPORTED_DISPLAYS = 17, 18, 19 + +_SPI_CHUNK_SIZE = 4096 +_SPI_COMMAND = 0 +_SPI_DATA = 1 + +_RESOLUTION = { + (400, 300): (400, 300, 0, 0, 0) +} + + +class Inky: + """Inky e-Ink Display Driver.""" + + WHITE = 0 + BLACK = 1 + RED = 2 + YELLOW = 2 + + def __init__(self, resolution=(400, 300), colour='black', cs_pin=CS0_PIN, dc_pin=DC_PIN, reset_pin=RESET_PIN, busy_pin=BUSY_PIN, h_flip=False, v_flip=False, spi_bus=None, i2c_bus=None, gpio=None): # noqa: E501 + """Initialise an Inky Display. + + :param resolution: (width, height) in pixels, default: (400, 300) + :param colour: one of red, black or yellow, default: black + :param cs_pin: chip-select pin for SPI communication + :param dc_pin: data/command pin for SPI communication + :param reset_pin: device reset pin + :param busy_pin: device busy/wait pin + :param h_flip: enable horizontal display flip, default: False + :param v_flip: enable vertical display flip, default: False + + """ + self._spi_bus = spi_bus + self._i2c_bus = i2c_bus + + if resolution not in _RESOLUTION.keys(): + raise ValueError('Resolution {}x{} not supported!'.format(*resolution)) + + self.resolution = resolution + self.width, self.height = resolution + self.cols, self.rows, self.rotation, self.offset_x, self.offset_y = _RESOLUTION[resolution] + + if colour not in ('red', 'black', 'yellow'): + raise ValueError('Colour {} is not supported!'.format(colour)) + + self.colour = colour + self.eeprom = eeprom.read_eeprom(i2c_bus=i2c_bus) + self.lut = colour + + if self.eeprom is not None: + # Only support SSD1683 variants + if self.eeprom.display_variant not in SUPPORTED_DISPLAYS: + raise RuntimeError('This driver is not compatible with your board.') + if self.eeprom.width != self.width or self.eeprom.height != self.height: + raise ValueError('Supplied width/height do not match Inky: {}x{}'.format(self.eeprom.width, self.eeprom.height)) + + self.buf = numpy.zeros((self.cols, self.rows), dtype=numpy.uint8) + + self.border_colour = 0 + + self.dc_pin = dc_pin + self.reset_pin = reset_pin + self.busy_pin = busy_pin + self.cs_pin = cs_pin + self.h_flip = h_flip + self.v_flip = v_flip + + self._gpio = gpio + self._gpio_setup = False + + self._luts = { + 'black': [ + 0x02, 0x02, 0x01, 0x11, 0x12, 0x12, 0x22, 0x22, 0x66, 0x69, + 0x69, 0x59, 0x58, 0x99, 0x99, 0x88, 0x00, 0x00, 0x00, 0x00, + 0xF8, 0xB4, 0x13, 0x51, 0x35, 0x51, 0x51, 0x19, 0x01, 0x00 + ], + 'red': [ + 0x02, 0x02, 0x01, 0x11, 0x12, 0x12, 0x22, 0x22, 0x66, 0x69, + 0x69, 0x59, 0x58, 0x99, 0x99, 0x88, 0x00, 0x00, 0x00, 0x00, + 0xF8, 0xB4, 0x13, 0x51, 0x35, 0x51, 0x51, 0x19, 0x01, 0x00 + ], + 'yellow': [ + 0x02, 0x02, 0x01, 0x11, 0x12, 0x12, 0x22, 0x22, 0x66, 0x69, + 0x69, 0x59, 0x58, 0x99, 0x99, 0x88, 0x00, 0x00, 0x00, 0x00, + 0xF8, 0xB4, 0x13, 0x51, 0x35, 0x51, 0x51, 0x19, 0x01, 0x00 + ] + } + + def setup(self): + """Set up Inky GPIO and reset display.""" + if not self._gpio_setup: + if self._gpio is None: + try: + import RPi.GPIO as GPIO + self._gpio = GPIO + except ImportError: + raise ImportError('This library requires the RPi.GPIO module\nInstall with: sudo apt install python-rpi.gpio') + self._gpio.setmode(self._gpio.BCM) + self._gpio.setwarnings(False) + self._gpio.setup(self.dc_pin, self._gpio.OUT, initial=self._gpio.LOW, pull_up_down=self._gpio.PUD_OFF) + self._gpio.setup(self.reset_pin, self._gpio.OUT, initial=self._gpio.HIGH, pull_up_down=self._gpio.PUD_OFF) + self._gpio.setup(self.busy_pin, self._gpio.IN, pull_up_down=self._gpio.PUD_OFF) + + if self._spi_bus is None: + import spidev + self._spi_bus = spidev.SpiDev() + + self._spi_bus.open(0, self.cs_pin) + self._spi_bus.max_speed_hz = 10000000 # Should be good for 20MHz according to datasheet + + self._gpio_setup = True + + self._gpio.output(self.reset_pin, self._gpio.LOW) + time.sleep(0.5) + self._gpio.output(self.reset_pin, self._gpio.HIGH) + time.sleep(0.5) + + self._send_command(0x12) # Soft Reset + time.sleep(1.0) + self._busy_wait() + + def _busy_wait(self, timeout=5.0): + """Wait for busy/wait pin.""" + t_start = time.time() + while self._gpio.input(self.busy_pin): + time.sleep(0.01) + if time.time() - t_start >= timeout: + raise RuntimeError("Timeout waiting for busy signal to clear.") + + def _update(self, buf_a, buf_b, busy_wait=True): + """Update display. + + Dispatches display update to correct driver. + + :param buf_a: Black/White pixels + :param buf_b: Yellow/Red pixels + + """ + self.setup() + + self._send_command(ssd1683.DRIVER_CONTROL, [self.rows - 1, (self.rows - 1) >> 8, 0x00]) + # Set dummy line period + self._send_command(ssd1683.WRITE_DUMMY, [0x1B]) + # Set Line Width + self._send_command(ssd1683.WRITE_GATELINE, [0x0B]) + # Data entry squence (scan direction leftward and downward) + self._send_command(ssd1683.DATA_MODE, [0x03]) + # Set ram X start and end position + xposBuf = [0x00, self.cols // 8 - 1] + self._send_command(ssd1683.SET_RAMXPOS, xposBuf) + # Set ram Y start and end position + yposBuf = [0x00, 0x00, (self.rows - 1) & 0xFF, (self.rows - 1) >> 8] + self._send_command(ssd1683.SET_RAMYPOS, yposBuf) + # VCOM Voltage + self._send_command(ssd1683.WRITE_VCOM, [0x70]) + # Write LUT DATA + #self._send_command(ssd1683.WRITE_LUT, self._luts[self.lut]) + + if self.border_colour == self.BLACK: + self._send_command(ssd1683.WRITE_BORDER, 0b00000000) + # GS Transition + Waveform 00 + GSA 0 + GSB 0 + elif self.border_colour == self.RED and self.colour == 'red': + self._send_command(ssd1683.WRITE_BORDER, 0b00000110) + # GS Transition + Waveform 01 + GSA 1 + GSB 0 + elif self.border_colour == self.YELLOW and self.colour == 'yellow': + self._send_command(ssd1683.WRITE_BORDER, 0b00001111) + # GS Transition + Waveform 11 + GSA 1 + GSB 1 + elif self.border_colour == self.WHITE: + self._send_command(ssd1683.WRITE_BORDER, 0b00000001) + # GS Transition + Waveform 00 + GSA 0 + GSB 1 + + # Set RAM address to 0, 0 + self._send_command(ssd1683.SET_RAMXCOUNT, [0x00]) + self._send_command(ssd1683.SET_RAMYCOUNT, [0x00, 0x00]) + + for data in ((ssd1683.WRITE_RAM, buf_a), (ssd1683.WRITE_ALTRAM, buf_b)): + cmd, buf = data + self._send_command(cmd, buf) + + self._busy_wait() + self._send_command(ssd1683.MASTER_ACTIVATE) + + def set_pixel(self, x, y, v): + """Set a single pixel. + + :param x: x position on display + :param y: y position on display + :param v: colour to set + + """ + if v in (WHITE, BLACK, RED): + self.buf[y][x] = v + + def show(self, busy_wait=True): + """Show buffer on display. + + :param busy_wait: If True, wait for display update to finish before returning. + + """ + region = self.buf + + if self.v_flip: + region = numpy.fliplr(region) + + if self.h_flip: + region = numpy.flipud(region) + + if self.rotation: + region = numpy.rot90(region, self.rotation // 90) + + buf_a = numpy.packbits(numpy.where(region == BLACK, 0, 1)).tolist() + buf_b = numpy.packbits(numpy.where(region == RED, 1, 0)).tolist() + + self._update(buf_a, buf_b, busy_wait=busy_wait) + + def set_border(self, colour): + """Set the border colour.""" + if colour in (WHITE, BLACK, RED): + self.border_colour = colour + + def set_image(self, image): + """Copy an image to the display.""" + canvas = Image.new("P", (self.cols, self.rows)) + canvas.paste(image, (self.offset_x, self.offset_y)) + self.buf = numpy.array(canvas, dtype=numpy.uint8).reshape((self.rows, self.cols)) + + def _spi_write(self, dc, values): + """Write values over SPI. + + :param dc: whether to write as data or command + :param values: list of values to write + + """ + self._gpio.output(self.dc_pin, dc) + try: + self._spi_bus.xfer3(values) + except AttributeError: + for x in range(((len(values) - 1) // _SPI_CHUNK_SIZE) + 1): + offset = x * _SPI_CHUNK_SIZE + self._spi_bus.xfer(values[offset:offset + _SPI_CHUNK_SIZE]) + + def _send_command(self, command, data=None): + """Send command over SPI. + + :param command: command byte + :param data: optional list of values + + """ + self._spi_write(_SPI_COMMAND, [command]) + if data is not None: + self._send_data(data) + + def _send_data(self, data): + """Send data over SPI. + + :param data: list of values + + """ + if isinstance(data, int): + data = [data] + self._spi_write(_SPI_DATA, data) diff --git a/library/inky/ssd1683.py b/library/inky/ssd1683.py new file mode 100644 index 00000000..a48ea602 --- /dev/null +++ b/library/inky/ssd1683.py @@ -0,0 +1,34 @@ +"""Constants for SSD1608 driver IC.""" +DRIVER_CONTROL = 0x01 +GATE_VOLTAGE = 0x03 +SOURCE_VOLTAGE = 0x04 +DISPLAY_CONTROL = 0x07 +NON_OVERLAP = 0x0B +BOOSTER_SOFT_START = 0x0C +GATE_SCAN_START = 0x0F +DEEP_SLEEP = 0x10 +DATA_MODE = 0x11 +SW_RESET = 0x12 +TEMP_WRITE = 0x1A +TEMP_READ = 0x1B +TEMP_CONTROL = 0x18 +TEMP_LOAD = 0x1A +MASTER_ACTIVATE = 0x20 +DISP_CTRL1 = 0x21 +DISP_CTRL2 = 0x22 +WRITE_RAM = 0x24 +WRITE_ALTRAM = 0x26 +READ_RAM = 0x25 +VCOM_SENSE = 0x2B +VCOM_DURATION = 0x2C +WRITE_VCOM = 0x2C +READ_OTP = 0x2D +WRITE_LUT = 0x32 +WRITE_DUMMY = 0x3A +WRITE_GATELINE = 0x3B +WRITE_BORDER = 0x3C +SET_RAMXPOS = 0x44 +SET_RAMYPOS = 0x45 +SET_RAMXCOUNT = 0x4E +SET_RAMYCOUNT = 0x4F +NOP = 0xFF From 37d7ffa6a2342571ccab8bfb22fa51ff535f5e9a Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Fri, 2 Sep 2022 11:14:46 +0100 Subject: [PATCH 2/4] Update wHAT examples to use auto. --- examples/what/dither-image-what-1683.py | 66 ------------------------- examples/what/dither-image-what.py | 16 +++--- examples/what/quotes-what.py | 44 +++++++---------- 3 files changed, 25 insertions(+), 101 deletions(-) delete mode 100755 examples/what/dither-image-what-1683.py diff --git a/examples/what/dither-image-what-1683.py b/examples/what/dither-image-what-1683.py deleted file mode 100755 index 7e364bde..00000000 --- a/examples/what/dither-image-what-1683.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -from PIL import Image -from inky import InkyWHAT_SSD1683 as InkyWHAT - -print("""Inky wHAT: Dither image - -Converts and displays dithered images on Inky wHAT. -""") - -# Command line arguments to set display type and colour, and enter your name - -parser = argparse.ArgumentParser() -parser.add_argument('--colour', '-c', type=str, required=True, choices=["red", "black", "yellow"], help="ePaper display colour") -parser.add_argument('--image', '-i', type=str, required=True, help="Input image to be converted/displayed") -args = parser.parse_args() - -colour = args.colour -img_file = args.image - -# Set up the inky wHAT display and border colour - -inky_display = InkyWHAT((400, 300), colour) -inky_display.set_border(inky_display.WHITE) - -# Open our image file that was passed in from the command line - -img = Image.open(img_file) - -# Get the width and height of the image - -w, h = img.size - -# Calculate the new height and width of the image - -h_new = 300 -w_new = int((float(w) / h) * h_new) -w_cropped = 400 - -# Resize the image with high-quality resampling - -img = img.resize((w_new, h_new), resample=Image.LANCZOS) - -# Calculate coordinates to crop image to 400 pixels wide - -x0 = (w_new - w_cropped) / 2 -x1 = x0 + w_cropped -y0 = 0 -y1 = h_new - -# Crop image - -img = img.crop((x0, y0, x1, y1)) - -# Convert the image to use a white / black / red colour palette - -pal_img = Image.new("P", (1, 1)) -pal_img.putpalette((255, 255, 255, 0, 0, 0, 255, 0, 0) + (0, 0, 0) * 252) - -img = img.convert("RGB").quantize(palette=pal_img) - -# Display the final image on Inky wHAT - -inky_display.set_image(img) -inky_display.show() diff --git a/examples/what/dither-image-what.py b/examples/what/dither-image-what.py index 655b9f4d..32d142d8 100755 --- a/examples/what/dither-image-what.py +++ b/examples/what/dither-image-what.py @@ -2,28 +2,26 @@ import argparse from PIL import Image -from inky import InkyWHAT +from inky.auto import auto print("""Inky wHAT: Dither image Converts and displays dithered images on Inky wHAT. """) -# Command line arguments to set display type and colour, and enter your name +# Set up the inky wHAT display and border colour + +inky_display = auto(ask_user=True, verbose=True) +inky_display.set_border(inky_display.WHITE) + +# Grab the image argument from the command line parser = argparse.ArgumentParser() -parser.add_argument('--colour', '-c', type=str, required=True, choices=["red", "black", "yellow"], help="ePaper display colour") parser.add_argument('--image', '-i', type=str, required=True, help="Input image to be converted/displayed") args = parser.parse_args() -colour = args.colour img_file = args.image -# Set up the inky wHAT display and border colour - -inky_display = InkyWHAT(colour) -inky_display.set_border(inky_display.WHITE) - # Open our image file that was passed in from the command line img = Image.open(img_file) diff --git a/examples/what/quotes-what.py b/examples/what/quotes-what.py index 89d81f76..899d5391 100755 --- a/examples/what/quotes-what.py +++ b/examples/what/quotes-what.py @@ -1,11 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import argparse import random import sys -from inky import InkyWHAT +from inky.auto import auto from PIL import Image, ImageFont, ImageDraw from font_source_serif_pro import SourceSerifProSemibold @@ -22,18 +21,16 @@ print("""This script requires the wikiquotes module. Install with: - sudo apt install python-lxml - sudo pip install wikiquotes + sudo apt install python3-lxml + python3 -m pip install wikiquotes """) sys.exit(1) -# Command line arguments to set display type and colour, and enter your name - -parser = argparse.ArgumentParser() -parser.add_argument('--colour', '-c', type=str, required=True, choices=["red", "black", "yellow"], help="ePaper display colour") -args = parser.parse_args() +# Set up the correct display and scaling factors -colour = args.colour +inky_display = auto(ask_user=True, verbose=True) +inky_display.set_border(inky_display.WHITE) +# inky_display.set_rotation(180) # This function will take a quote as a string, a width to fit # it into, and a font (one that's been loaded) and then reflow @@ -61,17 +58,12 @@ def reflow_quote(quote, width, font): return reflowed -# Set up the correct display and scaling factors -inky_display = InkyWHAT(colour) -inky_display.set_border(inky_display.WHITE) -# inky_display.set_rotation(180) - -w = inky_display.WIDTH -h = inky_display.HEIGHT +WIDTH = inky_display.width +HEIGHT = inky_display.height # Create a new canvas to draw on -img = Image.new("P", (inky_display.WIDTH, inky_display.HEIGHT)) +img = Image.new("P", (WIDTH, HEIGHT)) draw = ImageDraw.Draw(img) # Load the fonts @@ -112,8 +104,8 @@ def reflow_quote(quote, width, font): # Also define the max width and height for the quote. padding = 50 -max_width = w - padding -max_height = h - padding - author_font.getsize("ABCD ")[1] +max_width = WIDTH - padding +max_height = HEIGHT - padding - author_font.getsize("ABCD ")[1] below_max_length = False @@ -136,8 +128,8 @@ def reflow_quote(quote, width, font): # x- and y-coordinates for the top left of the quote -quote_x = (w - max_width) / 2 -quote_y = ((h - max_height) + (max_height - p_h - author_font.getsize("ABCD ")[1])) / 2 +quote_x = (WIDTH - max_width) / 2 +quote_y = ((HEIGHT - max_height) + (max_height - p_h - author_font.getsize("ABCD ")[1])) / 2 # x- and y-coordinates for the top left of the author @@ -148,16 +140,16 @@ def reflow_quote(quote, width, font): # Draw red rectangles top and bottom to frame quote -draw.rectangle((padding / 4, padding / 4, w - (padding / 4), quote_y - (padding / 4)), fill=inky_display.RED) -draw.rectangle((padding / 4, author_y + author_font.getsize("ABCD ")[1] + (padding / 4) + 5, w - (padding / 4), h - (padding / 4)), fill=inky_display.RED) +draw.rectangle((padding / 4, padding / 4, WIDTH - (padding / 4), quote_y - (padding / 4)), fill=inky_display.RED) +draw.rectangle((padding / 4, author_y + author_font.getsize("ABCD ")[1] + (padding / 4) + 5, WIDTH - (padding / 4), HEIGHT - (padding / 4)), fill=inky_display.RED) # Add some white hatching to the red rectangles to make # it look a bit more interesting hatch_spacing = 12 -for x in range(0, 2 * w, hatch_spacing): - draw.line((x, 0, x - w, h), fill=inky_display.WHITE, width=3) +for x in range(0, 2 * WIDTH, hatch_spacing): + draw.line((x, 0, x - WIDTH, HEIGHT), fill=inky_display.WHITE, width=3) # Write our quote and author to the canvas From 61bffc87cc23559343ae57fc3eb34f4bcbc11080 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Fri, 2 Sep 2022 11:14:59 +0100 Subject: [PATCH 3/4] eeprom: Python 3 fix. --- library/inky/eeprom.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/inky/eeprom.py b/library/inky/eeprom.py index 11422b56..69b92928 100644 --- a/library/inky/eeprom.py +++ b/library/inky/eeprom.py @@ -86,6 +86,9 @@ def encode(self): def to_list(self): """Return a list of bytes representing the EEPROM data structure.""" + result = self.encode() + if type(result) is bytes: + return result return [ord(c) for c in self.encode()] def set_color(self, color): From e6b4b97f1668e5213b162476f4fed295e90ba497 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 26 Sep 2022 16:15:04 +0100 Subject: [PATCH 4/4] SSD1683: Add to test coverage. Fix bugs and linting. --- examples/7color/stripes.py | 1 - examples/what/quotes-what.py | 17 ++++++- library/inky/auto.py | 10 ++-- library/inky/inky.py | 2 +- library/inky/inky_ssd1683.py | 2 +- library/inky/inky_uc8159.py | 7 ++- library/tests/test_auto.py | 88 ++++++++++++++++++++++++++++++++++-- library/tests/test_eeprom.py | 4 +- 8 files changed, 112 insertions(+), 19 deletions(-) diff --git a/examples/7color/stripes.py b/examples/7color/stripes.py index 82008d44..90692ff0 100755 --- a/examples/7color/stripes.py +++ b/examples/7color/stripes.py @@ -15,4 +15,3 @@ inky.show() # To simulate: # inky.wait_for_window_close() - diff --git a/examples/what/quotes-what.py b/examples/what/quotes-what.py index 899d5391..e7bbeaba 100755 --- a/examples/what/quotes-what.py +++ b/examples/what/quotes-what.py @@ -140,8 +140,21 @@ def reflow_quote(quote, width, font): # Draw red rectangles top and bottom to frame quote -draw.rectangle((padding / 4, padding / 4, WIDTH - (padding / 4), quote_y - (padding / 4)), fill=inky_display.RED) -draw.rectangle((padding / 4, author_y + author_font.getsize("ABCD ")[1] + (padding / 4) + 5, WIDTH - (padding / 4), HEIGHT - (padding / 4)), fill=inky_display.RED) +draw.rectangle( + ( + padding / 4, + padding / 4, + WIDTH - (padding / 4), + quote_y - (padding / 4) + ), fill=inky_display.RED) + +draw.rectangle( + ( + padding / 4, + author_y + author_font.getsize("ABCD ")[1] + (padding / 4) + 5, + WIDTH - (padding / 4), + HEIGHT - (padding / 4) + ), fill=inky_display.RED) # Add some white hatching to the red rectangles to make # it look a bit more interesting diff --git a/library/inky/auto.py b/library/inky/auto.py index 6239595d..b6fe2ccc 100644 --- a/library/inky/auto.py +++ b/library/inky/auto.py @@ -7,6 +7,10 @@ import argparse +DISPLAY_TYPES = ["what", "phat", "phatssd1608", "impressions", "7colour", "whatssd1683"] +DISPLAY_COLORS = ["red", "black", "yellow"] + + def auto(i2c_bus=None, ask_user=False, verbose=False): """Auto-detect Inky board from EEPROM and return an Inky class instance.""" _eeprom = eeprom.read_eeprom(i2c_bus=i2c_bus) @@ -33,8 +37,8 @@ def auto(i2c_bus=None, ask_user=False, verbose=False): print("Failed to detect an Inky board. Trying --type/--colour arguments instead...\n") parser = argparse.ArgumentParser() parser.add_argument('--simulate', '-s', action='store_true', default=False, help="Simulate Inky display") - parser.add_argument('--type', '-t', type=str, required=True, choices=["what", "phat", "phatssd1608", "impressions", "7colour", "whatssd1683"], help="Type of display") - parser.add_argument('--colour', '-c', type=str, required=False, choices=["red", "black", "yellow"], help="Display colour") + parser.add_argument('--type', '-t', type=str, required=True, choices=DISPLAY_TYPES, help="Type of display") + parser.add_argument('--colour', '-c', type=str, required=False, choices=DISPLAY_COLORS, help="Display colour") args, _ = parser.parse_known_args() if args.simulate: cls = None @@ -66,7 +70,7 @@ def auto(i2c_bus=None, ask_user=False, verbose=False): if args.type == "what": return InkyWHAT(args.colour) if args.type == "whatssd1683": - return InkyWHAT_SSD1683(args.colour) + return InkyWHAT_SSD1683(colour=args.colour) if args.type in ("impressions", "7colour"): return InkyUC8159() diff --git a/library/inky/inky.py b/library/inky/inky.py index a1e5ddc0..616f01ec 100644 --- a/library/inky/inky.py +++ b/library/inky/inky.py @@ -249,7 +249,7 @@ def setup(self): def _busy_wait(self): """Wait for busy/wait pin.""" - while(self._gpio.input(self.busy_pin) != self._gpio.LOW): + while self._gpio.input(self.busy_pin) != self._gpio.LOW: time.sleep(0.01) def _update(self, buf_a, buf_b, busy_wait=True): diff --git a/library/inky/inky_ssd1683.py b/library/inky/inky_ssd1683.py index 41633095..fcc15408 100644 --- a/library/inky/inky_ssd1683.py +++ b/library/inky/inky_ssd1683.py @@ -177,7 +177,7 @@ def _update(self, buf_a, buf_b, busy_wait=True): # VCOM Voltage self._send_command(ssd1683.WRITE_VCOM, [0x70]) # Write LUT DATA - #self._send_command(ssd1683.WRITE_LUT, self._luts[self.lut]) + # self._send_command(ssd1683.WRITE_LUT, self._luts[self.lut]) if self.border_colour == self.BLACK: self._send_command(ssd1683.WRITE_BORDER, 0b00000000) diff --git a/library/inky/inky_uc8159.py b/library/inky/inky_uc8159.py index 94526ab8..c4ff7f82 100644 --- a/library/inky/inky_uc8159.py +++ b/library/inky/inky_uc8159.py @@ -246,9 +246,9 @@ def setup(self): self._send_command( UC8159_PWR, [ - (0x06 << 3) | # ??? - not documented in UC8159 datasheet - (0x01 << 2) | # SOURCE_INTERNAL_DC_DC - (0x01 << 1) | # GATE_INTERNAL_DC_DC + (0x06 << 3) | # ??? - not documented in UC8159 datasheet # noqa: W504 + (0x01 << 2) | # SOURCE_INTERNAL_DC_DC # noqa: W504 + (0x01 << 1) | # GATE_INTERNAL_DC_DC # noqa: W504 (0x01), # LV_SOURCE_INTERNAL_DC_DC 0x00, # VGx_20V 0x23, # UC8159_7C @@ -295,7 +295,6 @@ def setup(self): def _busy_wait(self, timeout=40.0): """Wait for busy/wait pin.""" - # If the busy_pin is *high* (pulled up by host) # then assume we're not getting a signal from inky # and wait the timeout period to be safe. diff --git a/library/tests/test_auto.py b/library/tests/test_auto.py index 5871c8b0..8ee53364 100644 --- a/library/tests/test_auto.py +++ b/library/tests/test_auto.py @@ -3,12 +3,37 @@ import sys +DISPLAY_VARIANT = [ + None, + 'Red pHAT (High-Temp)', + 'Yellow wHAT', + 'Black wHAT', + 'Black pHAT', + 'Yellow pHAT', + 'Red wHAT', + 'Red wHAT (High-Temp)', + 'Red wHAT', + None, + 'Black pHAT (SSD1608)', + 'Red pHAT (SSD1608)', + 'Yellow pHAT (SSD1608)', + None, + '7-Colour (UC8159)', + '7-Colour 640x400 (UC8159)', + '7-Colour 640x400 (UC8159)', + 'Black wHAT (SSD1683)', + 'Red wHAT (SSD1683)', + 'Yellow wHAT (SSD1683)' +] + + +@pytest.mark.parametrize('verbose', [True, False]) @pytest.mark.parametrize('inky_colour', ['black', 'red', 'yellow', None]) -@pytest.mark.parametrize('inky_type', ['phat', 'what', 'phatssd1608', 'impressions', '7colour']) -def test_auto_fallback(spidev, smbus2, PIL, inky_type, inky_colour): +@pytest.mark.parametrize('inky_type', ['phat', 'what', 'phatssd1608', 'impressions', '7colour', 'whatssd1683']) +def test_auto_fallback(spidev, smbus2, PIL, inky_type, inky_colour, verbose): """Test auto init of 'phat', 'black'.""" + from inky import InkyPHAT, InkyPHAT_SSD1608, InkyWHAT, Inky7Colour, InkyWHAT_SSD1683 from inky import auto - from inky import InkyPHAT, InkyPHAT_SSD1608, InkyWHAT, Inky7Colour if inky_type in ['impressions', '7colour']: if inky_colour is not None: @@ -18,11 +43,64 @@ def test_auto_fallback(spidev, smbus2, PIL, inky_type, inky_colour): pytest.skip('PHAT/WHAT must specify colour') sys.argv[1:3] = ['--type', inky_type] + if inky_colour is not None: sys.argv[3:5] = ['--colour', inky_colour] - inky_class = {'phat': InkyPHAT, 'what': InkyWHAT, 'phatssd1608': InkyPHAT_SSD1608, 'impressions': Inky7Colour, '7colour': Inky7Colour}[inky_type] - inky = auto(ask_user=True) + inky_class = { + 'phat': InkyPHAT, + 'what': InkyWHAT, + 'phatssd1608': InkyPHAT_SSD1608, + 'impressions': Inky7Colour, + '7colour': Inky7Colour, + 'whatssd1683': InkyWHAT_SSD1683}[inky_type] + + inky = auto(ask_user=True, verbose=verbose) assert isinstance(inky, inky_class) is True if inky_colour is not None: assert inky.colour == inky_colour + + +@pytest.mark.parametrize('inky_display', enumerate(DISPLAY_VARIANT)) +def test_auto(spidev, smbus2_eeprom, PIL, inky_display): + """Test auto init of 'phat', 'black'.""" + from inky import InkyPHAT, InkyPHAT_SSD1608, InkyWHAT, Inky7Colour, InkyWHAT_SSD1683 + from inky import auto + from inky import eeprom + + display_id, display_name = inky_display + + if display_name is None: + pytest.skip('Skipping unsupported display ID') + + inky_class, inky_colour, inky_size = [ + (None, None, None), + (InkyPHAT, 'red', (212, 104)), + (InkyWHAT, 'yellow', (400, 300)), + (InkyWHAT, 'black', (400, 300)), + (InkyPHAT, 'black', (212, 104)), + (InkyPHAT, 'yellow', (212, 104)), + (InkyWHAT, 'red', (400, 300)), + (InkyWHAT, 'red', (400, 300)), + (InkyWHAT, 'red', (400, 300)), + (None, None, None), + (InkyPHAT_SSD1608, 'black', (250, 122)), + (InkyPHAT_SSD1608, 'red', (250, 122)), + (InkyPHAT_SSD1608, 'yellow', (250, 122)), + (None, None, None), + (Inky7Colour, '7colour', (600, 448)), + (Inky7Colour, '7colour', (600, 448)), + (Inky7Colour, '7colour', (600, 448)), + (InkyWHAT_SSD1683, 'black', (400, 300)), + (InkyWHAT_SSD1683, 'red', (400, 300)), + (InkyWHAT_SSD1683, 'yellow', (400, 300)) + ][display_id] + + width, height = inky_size + + eeprom_data = eeprom.EPDType(width, height, inky_colour, 0, display_id).encode() + + smbus2_eeprom.SMBus(1).read_i2c_block_data.return_value = eeprom_data + + inky = auto() + assert isinstance(inky, inky_class) diff --git a/library/tests/test_eeprom.py b/library/tests/test_eeprom.py index 545528f0..c310848f 100644 --- a/library/tests/test_eeprom.py +++ b/library/tests/test_eeprom.py @@ -2,7 +2,7 @@ def test_eeprom_7color_5_7_inch(spidev, smbus2_eeprom, PIL): - """Test EEPROM for 7color 5.7" Inky""" + """Test EEPROM for 7color 5.7" Inky.""" from inky.inky_uc8159 import Inky from inky.eeprom import EPDType @@ -16,7 +16,7 @@ def test_eeprom_7color_5_7_inch(spidev, smbus2_eeprom, PIL): def test_eeprom_7color_4_inch(spidev, smbus2_eeprom, PIL): - """Test EEPROM for 7color 4" Inky""" + """Test EEPROM for 7color 4" Inky.""" from inky.inky_uc8159 import Inky from inky.eeprom import EPDType