diff --git a/examples/spectra6/buttons.py b/examples/spectra6/buttons.py new file mode 100755 index 00000000..d6fab615 --- /dev/null +++ b/examples/spectra6/buttons.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 + +import gpiod +import gpiodevice +from gpiod.line import Bias, Direction, Edge + +print( + """buttons.py - Detect which button has been pressed + +This example should demonstrate how to: + 1. set up gpiod to read buttons, + 2. determine which button has been pressed + +Press Ctrl+C to exit! + +""" +) + +# GPIO pins for each button (from top to bottom) +# These will vary depending on platform and the ones +# below should be correct for Raspberry Pi 5. +# Run "gpioinfo" to find out what yours might be. +# +# Raspberry Pi 5 Header pins used by Inky Impression: +# PIN29, PIN31, PIN36, PIN18. +# These header pins correspond to BCM GPIO numbers: +# GPIO05, GPIO06, GPIO16, GPIO24. +# These GPIO numbers are what is used below and not the +# header pin numbers. + +SW_A = 5 +SW_B = 6 +SW_C = 16 # Set this value to '25' if you're using a Impression 13.3" +SW_D = 24 + +BUTTONS = [SW_A, SW_B, SW_C, SW_D] + +# These correspond to buttons A, B, C and D respectively +LABELS = ["A", "B", "C", "D"] + +# Create settings for all the input pins, we want them to be inputs +# with a pull-up and a falling edge detection. +INPUT = gpiod.LineSettings(direction=Direction.INPUT, bias=Bias.PULL_UP, edge_detection=Edge.FALLING) + +# Find the gpiochip device we need, we'll use +# gpiodevice for this, since it knows the right device +# for its supported platforms. +chip = gpiodevice.find_chip_by_platform() + +# Build our config for each pin/line we want to use +OFFSETS = [chip.line_offset_from_id(id) for id in BUTTONS] +line_config = dict.fromkeys(OFFSETS, INPUT) + +# Request the lines, *whew* +request = chip.request_lines(consumer="spectra6-buttons", config=line_config) + + +# "handle_button" will be called every time a button is pressed +# It receives one argument: the associated gpiod event object. +def handle_button(event): + index = OFFSETS.index(event.line_offset) + gpio_number = BUTTONS[index] + label = LABELS[index] + print(f"Button press detected on GPIO #{gpio_number} label: {label}") + + +while True: + for event in request.read_edge_events(): + handle_button(event) diff --git a/examples/spectra6/image.py b/examples/spectra6/image.py new file mode 100755 index 00000000..3d2ab1cc --- /dev/null +++ b/examples/spectra6/image.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 + +import argparse +import pathlib +import sys + +from PIL import Image + +from inky.auto import auto + +parser = argparse.ArgumentParser() + +parser.add_argument("--saturation", "-s", type=float, default=0.5, help="Colour palette saturation") +parser.add_argument("--file", "-f", type=pathlib.Path, help="Image file") + +inky = auto(ask_user=True, verbose=True) + +args, _ = parser.parse_known_args() + +saturation = args.saturation + +if not args.file: + print(f"""Usage: + {sys.argv[0]} --file image.png (--saturation 0.5)""") + sys.exit(1) + +image = Image.open(args.file) +resizedimage = image.resize(inky.resolution) + +try: + inky.set_image(resizedimage, saturation=saturation) +except TypeError: + inky.set_image(resizedimage) + +inky.show() diff --git a/examples/spectra6/led.py b/examples/spectra6/led.py new file mode 100755 index 00000000..d51fec99 --- /dev/null +++ b/examples/spectra6/led.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +import time + +import gpiod +import gpiodevice +from gpiod.line import Bias, Direction, Value + +print( + """\nled.py - Blink the LED! + +Press Ctrl+C to exit! + +""" +) + +LED_PIN = 13 + +# Find the gpiochip device we need, we'll use +# gpiodevice for this, since it knows the right device +# for its supported platforms. +chip = gpiodevice.find_chip_by_platform() + +# Setup for the LED pin +led = chip.line_offset_from_id(LED_PIN) +gpio = chip.request_lines(consumer="inky", config={led: gpiod.LineSettings(direction=Direction.OUTPUT, bias=Bias.DISABLED)}) + +while True: + gpio.set_value(led, Value.ACTIVE) + time.sleep(1) + gpio.set_value(led, Value.INACTIVE) + time.sleep(1) diff --git a/examples/spectra6/stripes.py b/examples/spectra6/stripes.py new file mode 100755 index 00000000..e407d8b3 --- /dev/null +++ b/examples/spectra6/stripes.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 + +from inky.auto import auto + +inky = auto(ask_user=True, verbose=True) + +COLOURS = [0, 1, 2, 3, 5, 6] + +for y in range(inky.height - 1): + c = y // (inky.height // 6) + for x in range(inky.width - 1): + inky.set_pixel(x, y, COLOURS[c]) + +inky.show() diff --git a/inky/__init__.py b/inky/__init__.py old mode 100644 new mode 100755 index 1b0b36d9..2c8c0bf2 --- a/inky/__init__.py +++ b/inky/__init__.py @@ -4,6 +4,8 @@ from .auto import auto # noqa: F401 from .inky import BLACK, RED, WHITE, YELLOW # noqa: F401 from .inky_ac073tc1a import Inky as Inky_Impressions_7 # noqa: F401 +from .inky_e673 import Inky as InkyE673 # noqa: F401 +from .inky_el133uf1 import Inky as InkyEL133UF1 # noqa: F401 from .inky_ssd1683 import Inky as InkyWHAT_SSD1683 # noqa: F401 from .inky_uc8159 import Inky as Inky7Colour # noqa: F401 from .mock import InkyMockPHAT, InkyMockWHAT # noqa: F401 diff --git a/inky/auto.py b/inky/auto.py old mode 100644 new mode 100755 index f37ee459..c8af3a0c --- a/inky/auto.py +++ b/inky/auto.py @@ -3,12 +3,14 @@ from . import eeprom from .inky_ac073tc1a import Inky as InkyAC073TC1A # noqa: F401 +from .inky_e673 import Inky as InkyE673 # noqa: F401 +from .inky_el133uf1 import Inky as InkyEL133UF1 # noqa: F401 from .inky_ssd1683 import Inky as InkyWHAT_SSD1683 # noqa: F401 from .inky_uc8159 import Inky as InkyUC8159 # noqa: F401 from .phat import InkyPHAT, InkyPHAT_SSD1608 # noqa: F401 from .what import InkyWHAT # noqa: F401 -DISPLAY_TYPES = ["what", "phat", "phatssd1608", "impressions", "7colour", "whatssd1683", "impressions73"] +DISPLAY_TYPES = ["what", "phat", "phatssd1608", "impressions", "7colour", "whatssd1683", "impressions73", "spectra13", "spectra73"] DISPLAY_COLORS = ["red", "black", "yellow"] @@ -34,6 +36,10 @@ def auto(i2c_bus=None, ask_user=False, verbose=False): return InkyWHAT_SSD1683((400, 300), _eeprom.get_color()) if _eeprom.display_variant == 20: return InkyAC073TC1A(resolution=(800, 480)) + if _eeprom.display_variant == 21: + return InkyEL133UF1(resolution=(1600, 1200)) + if _eeprom.display_variant == 22: + return InkyE673(resolution=(800, 480)) if ask_user: if verbose: @@ -78,6 +84,10 @@ def auto(i2c_bus=None, ask_user=False, verbose=False): return InkyUC8159() if args.type == "impressions73": return InkyAC073TC1A() + if args.type == "spectra13": + return InkyEL133UF1() + if args.type == "spectra73": + return InkyE673() if _eeprom is None: raise RuntimeError("No EEPROM detected! You must manually initialise your Inky board.") diff --git a/inky/eeprom.py b/inky/eeprom.py index 65a11446..bd4cd087 100644 --- a/inky/eeprom.py +++ b/inky/eeprom.py @@ -31,13 +31,15 @@ "Red wHAT (SSD1683)", "Yellow wHAT (SSD1683)", "7-Colour 800x480 (AC073TC1A)", + "Spectra 6 13.3 1600 x 1200 (EL133UF1)", + "Spectra 6 7.3 800 x 480 (E673)" ] class EPDType: """Class to represent EPD EEPROM structure.""" - valid_colors = [None, "black", "red", "yellow", None, "7colour"] + valid_colors = [None, "black", "red", "yellow", None, "7colour", "spectra6"] def __init__(self, width, height, color, pcb_variant, display_variant, write_time=None): """Initialise new EEPROM data structure.""" diff --git a/inky/inky_e673.py b/inky/inky_e673.py new file mode 100755 index 00000000..43c6c1d2 --- /dev/null +++ b/inky/inky_e673.py @@ -0,0 +1,363 @@ +"""Inky e-Ink Display Driver.""" +import time +import warnings + +import gpiod +import gpiodevice +import numpy +from gpiod.line import Bias, Direction, Value +from PIL import Image + +from . import eeprom + +BLACK = 0 +WHITE = 1 +YELLOW = 2 +RED = 3 +BLUE = 5 +GREEN = 6 + +DESATURATED_PALETTE = [ + [0, 0, 0], + [255, 255, 255], + [255, 255, 0], + [255, 0, 0], + [0, 0, 255], + [0, 255, 0], + [255, 255, 255]] + +SATURATED_PALETTE = [ + [0, 0, 0], + [161, 164, 165], + [208, 190, 71], + [156, 72, 75], + [61, 59, 94], + [58, 91, 70], + [255, 255, 255]] + +RESET_PIN = 27 # PIN13 +BUSY_PIN = 17 # PIN11 +DC_PIN = 22 # PIN15 + +MOSI_PIN = 10 +SCLK_PIN = 11 +CS0_PIN = 8 + +EL673_PSR = 0x00 +EL673_PWR = 0x01 +EL673_POF = 0x02 +EL673_POFS = 0x03 +EL673_PON = 0x04 +EL673_BTST1 = 0x05 +EL673_BTST2 = 0x06 +EL673_DSLP = 0x07 +EL673_BTST3 = 0x08 +EL673_DTM1 = 0x10 +EL673_DSP = 0x11 +EL673_DRF = 0x12 +EL673_PLL = 0x30 +EL673_CDI = 0x50 +EL673_TCON = 0x60 +EL673_TRES = 0x61 +EL673_REV = 0x70 +EL673_VDCS = 0x82 +EL673_PWS = 0xE3 + +_SPI_CHUNK_SIZE = 4096 + +_RESOLUTION_7_3_INCH = (800, 480) # Inky Impression 7.3 (Spectra 6)" + +_RESOLUTION = { + _RESOLUTION_7_3_INCH: (_RESOLUTION_7_3_INCH[0], _RESOLUTION_7_3_INCH[1], 0, 0, 0, 0b01), +} + + +class Inky: + """Inky e-Ink Display Driver.""" + + BLACK = 0 + WHITE = 1 + YELLOW = 2 + RED = 3 + BLUE = 5 + GREEN = 6 + + WIDTH = 0 + HEIGHT = 0 + + DESATURATED_PALETTE = [ + [0, 0, 0], + [255, 255, 255], + [255, 255, 0], + [255, 0, 0], + [0, 0, 255], + [0, 255, 0], + [255, 255, 255]] + + SATURATED_PALETTE = [ + [0, 0, 0], + [161, 164, 165], + [208, 190, 71], + [156, 72, 75], + [61, 59, 94], + [58, 91, 70], + [255, 255, 255]] + + def __init__(self, resolution=None, colour="multi", 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: (800, 480) + :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 + self.eeprom = eeprom.read_eeprom(i2c_bus=i2c_bus) + + # Check for supported display variant and select the correct resolution + if resolution is None: + resolution = _RESOLUTION_7_3_INCH + + if resolution not in _RESOLUTION.keys(): + raise ValueError(f"Resolution {resolution[0]}x{resolution[1]} not supported!") + + self.resolution = resolution + self.width, self.height = resolution + self.WIDTH, self.HEIGHT = resolution + self.border_colour = WHITE + self.cols, self.rows, self.rotation, self.offset_x, self.offset_y, self.resolution_setting = _RESOLUTION[resolution] + + if colour not in ("multi"): + raise ValueError(f"Colour {colour} is not supported!") + + self.colour = colour + self.lut = colour + + self.buf = numpy.zeros((self.rows, self.cols), dtype=numpy.uint8) + + self.dc_pin = dc_pin + self.reset_pin = reset_pin + self.busy_pin = busy_pin + self.cs_pin = cs_pin + try: + self.cs_channel = [8, 7].index(cs_pin) + except ValueError: + self.cs_channel = 0 + self.h_flip = h_flip + self.v_flip = v_flip + + self._gpio = gpio + self._gpio_setup = False + + self._luts = None + + def _palette_blend(self, saturation, dtype="uint8"): + saturation = float(saturation) + palette = [] + for i in range(6): + rs, gs, bs = [c * saturation for c in self.SATURATED_PALETTE[i]] + rd, gd, bd = [c * (1.0 - saturation) for c in self.DESATURATED_PALETTE[i]] + if dtype == "uint8": + palette += [int(rs + rd), int(gs + gd), int(bs + bd)] + if dtype == "uint24": + palette += [(int(rs + rd) << 16) | (int(gs + gd) << 8) | int(bs + bd)] + return palette + + def setup(self): + """Set up Inky GPIO and reset display.""" + if not self._gpio_setup: + if self._gpio is None: + gpiochip = gpiodevice.find_chip_by_platform() + gpiodevice.friendly_errors = True + if gpiodevice.check_pins_available(gpiochip, { + "Chip Select": self.cs_pin, + "Data/Command": self.dc_pin, + "Reset": self.reset_pin, + "Busy": self.busy_pin + }): + self.cs_pin = gpiochip.line_offset_from_id(self.cs_pin) + self.dc_pin = gpiochip.line_offset_from_id(self.dc_pin) + self.reset_pin = gpiochip.line_offset_from_id(self.reset_pin) + self.busy_pin = gpiochip.line_offset_from_id(self.busy_pin) + self._gpio = gpiochip.request_lines(consumer="inky", config={ + self.cs_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE, bias=Bias.DISABLED), + self.dc_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.INACTIVE, bias=Bias.DISABLED), + self.reset_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE, bias=Bias.DISABLED), + self.busy_pin: gpiod.LineSettings(direction=Direction.INPUT, bias=Bias.PULL_UP) + }) + + if self._spi_bus is None: + import spidev + self._spi_bus = spidev.SpiDev() + + self._spi_bus.open(0, self.cs_channel) + try: + self._spi_bus.no_cs = True + except OSError: + warnings.warn("SPI: Cannot disable chip-select!") + self._spi_bus.max_speed_hz = 1000000 + + self._gpio_setup = True + + self._gpio.set_value(self.reset_pin, Value.INACTIVE) + time.sleep(0.03) + self._gpio.set_value(self.reset_pin, Value.ACTIVE) + time.sleep(0.03) + + self._busy_wait(5.0) + + self._send_command(0xAA, [0x49, 0x55, 0x20, 0x08, 0x09, 0x18]) + self._send_command(EL673_PWR, [0x3F]) + self._send_command(EL673_PSR, [0x5F, 0x69]) + + self._send_command(EL673_BTST1, [0x40, 0x1F, 0x1F, 0x2C]) + self._send_command(EL673_BTST3, [0x6F, 0x1F, 0x1F, 0x22]) + self._send_command(EL673_BTST2, [0x6F, 0x1F, 0x17, 0x17]) + + self._send_command(EL673_POFS, [0x00, 0x54, 0x00, 0x44]) + self._send_command(EL673_TCON, [0x02, 0x00]) + self._send_command(EL673_PLL, [0x08]) + self._send_command(EL673_CDI, [0x3F]) + self._send_command(EL673_TRES, [0x03, 0x20, 0x01, 0xE0]) + self._send_command(EL673_PWS, [0x2F]) + self._send_command(EL673_VDCS, [0x01]) + + def _busy_wait(self, timeout=40.0): + """Wait for busy/wait pin.""" + t_start = time.time() + while self._gpio.get_value(self.busy_pin) == Value.ACTIVE: + time.sleep(0.1) + if time.time() - t_start > timeout: + warnings.warn(f"Busy Wait: Timed out after {timeout:0.2f}s") + return + + def _update(self, buf): + """Update display. + + Dispatches display update to correct driver. + + """ + self.setup() + time.sleep(0.1) + + self._send_command(EL673_DTM1, buf) + time.sleep(0.1) + + self._send_command(EL673_PON) + self._busy_wait(0.1) + + # second setting of the BTST2 register + self._send_command(EL673_BTST2, [0x6F, 0x1F, 0x17, 0x49]) + time.sleep(0.03) + + self._send_command(EL673_DRF, [0x00]) + self._busy_wait(32.0) + + self._send_command(EL673_POF, [0x00]) + self._busy_wait(0.2) + + 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 + + """ + self.buf[y][x] = v & 0x07 + + 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 = region.flatten() + + buf = ((buf[::2] << 4) & 0xF0) | (buf[1::2] & 0x0F) + + self._update(buf.astype("uint8").tolist()) + + def set_border(self, colour): + """Set the border colour.""" + if colour in (BLACK, WHITE, GREEN, BLUE, RED, YELLOW): + self.border_colour = colour + + def set_image(self, image, saturation=0.5): + """Copy an image to the display. + + :param image: PIL image to copy, must be 800x480 + :param saturation: Saturation for quantization palette - higher value results in a more saturated image + + """ + if not image.size == (self.width, self.height): + raise ValueError(f"Image must be ({self.width}x{self.height}) pixels!") + if not image.mode == "P": + palette = self._palette_blend(saturation) + # Image size doesn't matter since it's just the palette we're using + palette_image = Image.new("P", (1, 1)) + # Set our 6 colour palette and zero out the remaining colours + palette_image.putpalette(palette + [0, 0, 0] * 248) + # Force source image data to be loaded for `.im` to work + image.load() + image = image.im.convert("P", True, palette_image.im) + + remap = numpy.array([0, 1, 2, 3, 5, 6]) + self.buf = remap[numpy.array(image, 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.set_value(self.cs_pin, Value.INACTIVE) + self._gpio.set_value(self.dc_pin, Value.ACTIVE if dc else Value.INACTIVE) + + if isinstance(values, str): + values = [ord(c) for c in values] + + 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]) + + self._gpio.set_value(self.cs_pin, Value.ACTIVE) + + def _send_command(self, command, data=None): + """Send command over SPI. + :param command: command byte + :param data: optional list of values + """ + + self._gpio.set_value(self.cs_pin, Value.INACTIVE) + self._gpio.set_value(self.dc_pin, Value.INACTIVE) + time.sleep(0.3) + self._spi_bus.xfer3([command]) + + if data is not None: + self._gpio.set_value(self.dc_pin, Value.ACTIVE) + self._spi_bus.xfer3(data) + + self._gpio.set_value(self.cs_pin, Value.ACTIVE) + self._gpio.set_value(self.dc_pin, Value.INACTIVE) diff --git a/inky/inky_el133uf1.py b/inky/inky_el133uf1.py new file mode 100644 index 00000000..b4401864 --- /dev/null +++ b/inky/inky_el133uf1.py @@ -0,0 +1,375 @@ +"""Inky e-Ink Display Driver.""" +import time +import warnings + +import gpiod +import gpiodevice +import numpy +from gpiod.line import Bias, Direction, Value +from PIL import Image + +from . import eeprom + +BLACK = 0 +WHITE = 1 +YELLOW = 2 +RED = 3 +BLUE = 5 +GREEN = 6 + +DESATURATED_PALETTE = [ + [0, 0, 0], + [255, 255, 255], + [255, 255, 0], + [255, 0, 0], + [0, 0, 255], + [0, 255, 0], + [255, 255, 255]] + +SATURATED_PALETTE = [ + [0, 0, 0], + [161, 164, 165], + [208, 190, 71], + [156, 72, 75], + [61, 59, 94], + [58, 91, 70], + [255, 255, 255]] + +RESET_PIN = 27 # PIN13 +BUSY_PIN = 17 # PIN11 +DC_PIN = 22 # PIN15 + +MOSI_PIN = 10 +SCLK_PIN = 11 +CS0_PIN = 26 +CS1_PIN = 16 + +CS0_SEL = 0b01 +CS1_SEL = 0b10 +CS_BOTH_SEL = CS0_SEL | CS1_SEL + +EL133UF1_PSR = 0x00 +EL133UF1_PWR = 0x01 +EL133UF1_POF = 0x02 +EL133UF1_PON = 0x04 +EL133UF1_BTST_N = 0x05 +EL133UF1_BTST_P = 0x06 +EL133UF1_DTM = 0x10 +EL133UF1_DRF = 0x12 +EL133UF1_PLL = 0x30 +EL133UF1_TSC = 0x40 +EL133UF1_TSE = 0x41 +EL133UF1_TSW = 0x42 +EL133UF1_TSR = 0x43 +EL133UF1_CDI = 0x50 +EL133UF1_LPD = 0x51 +EL133UF1_TCON = 0x60 +EL133UF1_TRES = 0x61 +EL133UF1_DAM = 0x65 +EL133UF1_REV = 0x70 +EL133UF1_FLG = 0x71 +EL133UF1_AMV = 0x80 +EL133UF1_VV = 0x81 +EL133UF1_VDCS = 0x82 +EL133UF1_PTLW = 0x83 +EL133UF1_ANTM = 0x74 +EL133UF1_AGID = 0x86 +EL133UF1_PWS = 0xE3 +EL133UF1_TSSET = 0xE5 +EL133UF1_CMD66 = 0xF0 +EL133UF1_CCSET = 0xE0 +EL133UF1_BOOST_VDDP_EN = 0xB7 +EL133UF1_EN_BUF = 0xB6 +EL133UF1_TFT_VCOM_POWER = 0xB1 +EL133UF1_BUCK_BOOST_VDDN = 0xB0 + +_RESOLUTION_13_3_INCH = (1600, 1200) # Inky Impression 13 (Spectra 6)" + +_RESOLUTION = { + _RESOLUTION_13_3_INCH: (_RESOLUTION_13_3_INCH[0], _RESOLUTION_13_3_INCH[1], 0, 0, 0, 0b01) +} + + +class Inky: + """Inky e-Ink Display Driver.""" + + BLACK = 0 + WHITE = 1 + YELLOW = 2 + RED = 3 + BLUE = 5 + GREEN = 6 + + WIDTH = 0 + HEIGHT = 0 + + DESATURATED_PALETTE = [ + [0, 0, 0], + [255, 255, 255], + [255, 255, 0], + [255, 0, 0], + [0, 0, 255], + [0, 255, 0], + [255, 255, 255]] + + SATURATED_PALETTE = [ + [0, 0, 0], + [161, 164, 165], + [208, 190, 71], + [156, 72, 75], + [61, 59, 94], + [58, 91, 70], + [255, 255, 255]] + + def __init__(self, resolution=None, colour="multi", cs_pin_0=CS0_PIN, cs_pin_1=CS1_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: (1600, 1200) + :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 + self.eeprom = eeprom.read_eeprom(i2c_bus=i2c_bus) + + # Check for supported display variant and select the correct resolution + if resolution is None: + if self.eeprom is not None and self.eeprom.display_variant == 21: + resolution = [_RESOLUTION_13_3_INCH, None, _RESOLUTION_13_3_INCH][self.eeprom.display_variant] + else: + resolution = _RESOLUTION_13_3_INCH + + if resolution not in _RESOLUTION.keys(): + raise ValueError(f"Resolution {resolution[0]}x{resolution[1]} not supported!") + + self.resolution = resolution + self.width, self.height = resolution + self.WIDTH, self.HEIGHT = resolution + self.border_colour = WHITE + self.cols, self.rows, self.rotation, self.offset_x, self.offset_y, self.resolution_setting = _RESOLUTION[resolution] + + if colour not in ("multi"): + raise ValueError(f"Colour {colour} is not supported!") + + self.colour = colour + self.lut = colour + + self.buf = numpy.zeros((self.rows, self.cols), dtype=numpy.uint8) + + self.dc_pin = dc_pin + self.reset_pin = reset_pin + self.busy_pin = busy_pin + self.cs_pin_0 = cs_pin_0 + self.cs_pin_1 = cs_pin_1 + + try: + self.cs_channel = [8, 7].index(cs_pin_0) + except ValueError: + self.cs_channel = 0 + + self.h_flip = h_flip + self.v_flip = v_flip + + self._gpio = gpio + self._gpio_setup = False + + def _palette_blend(self, saturation, dtype="uint8"): + saturation = float(saturation) + palette = [] + for i in range(6): + rs, gs, bs = [c * saturation for c in self.SATURATED_PALETTE[i]] + rd, gd, bd = [c * (1.0 - saturation) for c in self.DESATURATED_PALETTE[i]] + if dtype == "uint8": + palette += [int(rs + rd), int(gs + gd), int(bs + bd)] + if dtype == "uint24": + palette += [(int(rs + rd) << 16) | (int(gs + gd) << 8) | int(bs + bd)] + return palette + + def setup(self): + """Set up Inky GPIO and reset display.""" + if not self._gpio_setup: + if self._gpio is None: + gpiochip = gpiodevice.find_chip_by_platform() + gpiodevice.friendly_errors = True + if gpiodevice.check_pins_available(gpiochip, { + "Chip Select 0": self.cs_pin_0, + "Chip Select 1": self.cs_pin_1, + "Data/Command": self.dc_pin, + "Reset": self.reset_pin, + "Busy": self.busy_pin + }): + + self.cs0_pin = gpiochip.line_offset_from_id(self.cs_pin_0) + self.cs1_pin = gpiochip.line_offset_from_id(self.cs_pin_1) + self.dc_pin = gpiochip.line_offset_from_id(self.dc_pin) + self.reset_pin = gpiochip.line_offset_from_id(self.reset_pin) + self.busy_pin = gpiochip.line_offset_from_id(self.busy_pin) + self._gpio = gpiochip.request_lines(consumer="inky", config={ + self.cs0_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE, bias=Bias.DISABLED), + self.cs1_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE, bias=Bias.DISABLED), + self.dc_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.INACTIVE, bias=Bias.DISABLED), + self.reset_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE, bias=Bias.DISABLED), + self.busy_pin: gpiod.LineSettings(direction=Direction.INPUT, bias=Bias.PULL_UP) + + }) + + if self._spi_bus is None: + import spidev + self._spi_bus = spidev.SpiDev() + + self._spi_bus.open(0, self.cs_channel) + try: + self._spi_bus.no_cs = True + except OSError: + warnings.warn("SPI: Cannot disable chip-select!") + self._spi_bus.max_speed_hz = 10000000 + + self._gpio_setup = True + + self._gpio.set_value(self.reset_pin, Value.INACTIVE) + time.sleep(0.03) + self._gpio.set_value(self.reset_pin, Value.ACTIVE) + time.sleep(0.03) + + self._busy_wait(10.0) + + self._send_command(EL133UF1_ANTM, CS0_SEL, [0xC0, 0x1C, 0x1C, 0xCC, 0xCC, 0xCC, 0x15, 0x15, 0x55]) + + self._send_command(EL133UF1_CMD66, CS_BOTH_SEL, [0x49, 0x55, 0x13, 0x5D, 0x05, 0x10]) + self._send_command(EL133UF1_PSR, CS_BOTH_SEL, [0xDF, 0x69]) + self._send_command(EL133UF1_PLL, CS_BOTH_SEL, [0x08]) + self._send_command(EL133UF1_CDI, CS_BOTH_SEL, [0xF7]) + self._send_command(EL133UF1_TCON, CS_BOTH_SEL, [0x03, 0x03]) + self._send_command(EL133UF1_AGID, CS_BOTH_SEL, [0x10]) + self._send_command(EL133UF1_PWS, CS_BOTH_SEL, [0x22]) + self._send_command(EL133UF1_CCSET, CS_BOTH_SEL, [0x01]) + self._send_command(EL133UF1_TRES, CS_BOTH_SEL, [0x04, 0xB0, 0x03, 0x20]) + + self._send_command(EL133UF1_PWR, CS0_SEL, [0x0F, 0x00, 0x28, 0x2C, 0x28, 0x38]) + self._send_command(EL133UF1_EN_BUF, CS0_SEL, [0x07]) + self._send_command(EL133UF1_BTST_P, CS0_SEL, [0xD8, 0x18]) + self._send_command(EL133UF1_BOOST_VDDP_EN, CS0_SEL, [0x01]) + self._send_command(EL133UF1_BTST_N, CS0_SEL, [0xD8, 0x18]) + self._send_command(EL133UF1_BUCK_BOOST_VDDN, CS0_SEL, [0x01]) + self._send_command(EL133UF1_TFT_VCOM_POWER, CS0_SEL, [0x02]) + + def _busy_wait(self, timeout=40.0): + """Wait for busy/wait pin.""" + t_start = time.time() + while self._gpio.get_value(self.busy_pin) == Value.ACTIVE: + time.sleep(0.1) + if time.time() - t_start > timeout: + warnings.warn(f"Busy Wait: Timed out after {timeout:0.2f}s") + return + + def _update(self, buf_a, buf_b): + """Update display. + Dispatches display update to correct driver. + """ + self.setup() + time.sleep(0.1) + + self._send_command(EL133UF1_DTM, CS0_SEL, buf_a) + time.sleep(0.1) + + self._send_command(EL133UF1_DTM, CS1_SEL, buf_b) + time.sleep(0.1) + + self._send_command(EL133UF1_PON, CS_BOTH_SEL) + self._busy_wait(0.2) + + self._send_command(EL133UF1_DRF, CS_BOTH_SEL, [0x00]) + self._busy_wait(32.0) + + self._send_command(EL133UF1_POF, CS_BOTH_SEL, [0x00]) + self._busy_wait(0.2) + + 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 + """ + self.buf[y][x] = v & 0x07 + + 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) + + region = numpy.rot90(region, -1) + buf_a = region[:, :600].flatten() + buf_b = region[:, 600:].flatten() + + buf_a = (((buf_a[::2] << 4) & 0xF0) | (buf_a[1::2] & 0x0F)).astype("uint8").tolist() + buf_b = (((buf_b[::2] << 4) & 0xF0) | (buf_b[1::2] & 0x0F)).astype("uint8").tolist() + + self._update(buf_a, buf_b) + + def set_border(self, colour): + """Set the border colour.""" + if colour in (BLACK, WHITE, GREEN, BLUE, RED, YELLOW): + self.border_colour = colour + + def set_image(self, image, saturation=0.5): + """Copy an image to the display. + :param image: PIL image to copy, must be 1600x1200 + :param saturation: Saturation for quantization palette - higher value results in a more saturated image + """ + if not image.size == (self.width, self.height): + raise ValueError(f"Image must be ({self.width}x{self.height}) pixels!") + + if not image.mode == "P": + palette = self._palette_blend(saturation) + # Image size doesn't matter since it's just the palette we're using + palette_image = Image.new("P", (1, 1)) + # Set our 7 colour palette (+ clear) and zero out the remaining colours + palette_image.putpalette(palette + [0, 0, 0] * 250) + # Force source image data to be loaded for `.im` to work + image.load() + image = image.im.convert("P", True, palette_image.im) + + remap = numpy.array([0, 1, 2, 3, 5, 6]) + self.buf = remap[numpy.array(image, dtype=numpy.uint8).reshape((self.rows, self.cols))] + + def _spi_write_bytes(self, data): + for x in range(((len(data) - 1) // 4096) + 1): + offset = x * 4096 + self._spi_bus.writebytes(data[offset:offset + 4096]) + + def _send_command(self, command, cs_sel=None, data=None): + """Send command over SPI. + :param command: command byte + :param data: optional list of values + """ + if cs_sel is not None: + if cs_sel & CS0_SEL: + self._gpio.set_value(self.cs0_pin, Value.INACTIVE) + if cs_sel & CS1_SEL: + self._gpio.set_value(self.cs1_pin, Value.INACTIVE) + + self._gpio.set_value(self.dc_pin, Value.INACTIVE) + time.sleep(0.3) + self._spi_bus.xfer3([command]) + + if data is not None: + self._gpio.set_value(self.dc_pin, Value.ACTIVE) + self._spi_bus.xfer3(data) + + self._gpio.set_value(self.cs0_pin, Value.ACTIVE) + self._gpio.set_value(self.cs1_pin, Value.ACTIVE) + self._gpio.set_value(self.dc_pin, Value.INACTIVE)