diff --git a/pwnagotchi/ui/hw/inky.py b/pwnagotchi/ui/hw/inky.py index 93c1428e..28f9d60e 100644 --- a/pwnagotchi/ui/hw/inky.py +++ b/pwnagotchi/ui/hw/inky.py @@ -38,7 +38,7 @@ class Inky(DisplayImpl): logging.info("THIS MAY BE POTENTIALLY DANGEROUS. NO WARRANTY IS PROVIDED") logging.info("USE THIS DISPLAY IN THIS MODE AT YOUR OWN RISK") - from pwnagotchi.ui.hw.libs.inkyphat.inkyphatfast import InkyPHATFast + from pwnagotchi.ui.hw.libs.pimoroni.inkyphat.inkyphatfast import InkyPHATFast self._display = InkyPHATFast('black') self._display.set_border(InkyPHATFast.BLACK) elif self.config['color'] == 'auto': @@ -84,4 +84,5 @@ class Inky(DisplayImpl): logging.exception("error while rendering on inky") def clear(self): - self._display.Clear() + pass + # self._display.clear() diff --git a/pwnagotchi/ui/hw/libs/pimoroni/inkyphatv2/inky.py b/pwnagotchi/ui/hw/libs/pimoroni/inkyphatv2/inky.py new file mode 100644 index 00000000..e53cdef8 --- /dev/null +++ b/pwnagotchi/ui/hw/libs/pimoroni/inkyphatv2/inky.py @@ -0,0 +1,404 @@ +"""Inky e-Ink Display Driver.""" +import time +import struct + +from . import eeprom + +try: + import numpy +except ImportError: + raise ImportError('This library requires the numpy module\nInstall with: sudo apt install python-numpy') + +# Display colour codes +WHITE = 0 +BLACK = 1 +RED = YELLOW = 2 + +# GPIO pins required by BCM number +RESET_PIN = 27 +BUSY_PIN = 17 +DC_PIN = 22 + +# In addition the following pins are used for SPI +# CS_PIN = 8 +# MOSI_PIN = 10 +# SCLK_PIN = 11 +# SCLK_PIN = 11 + +# SPI channel for device 0 +CS0 = 0 + +_SPI_CHUNK_SIZE = 4096 +_SPI_COMMAND = 0 +_SPI_DATA = 1 + +_RESOLUTION = { + (800, 480): (800, 480, 0), + (600, 448): (600, 448, 0), + (400, 300): (400, 300, 0), + (212, 104): (104, 212, -90), + (250, 122): (250, 122, -90), +} + + +class Inky: + """Inky e-Ink Display Driver. + + Generally it is more convenient to use either the :class:`inky.InkyPHAT` or :class:`inky.InkyWHAT` classes. + """ + + WHITE = 0 + BLACK = 1 + RED = 2 + YELLOW = 2 + + def __init__(self, resolution=(400, 300), colour='black', cs_channel=CS0, 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): + """Initialise an Inky Display. + + :param resolution: Display resolution (width, height) in pixels, default: (400, 300). + :type resolution: tuple(int, int) + :param str colour: One of 'red', 'black' or 'yellow', default: 'black'. + :param int cs_channel: Chip-select channel for SPI communication, default: `0`. + :param int dc_pin: Data/command pin for SPI communication, default: `22`. + :param int reset_pin: Device reset pin, default: `27`. + :param int busy_pin: Device busy/wait pin: `17`. + :param bool h_flip: Enable horizontal display flip, default: `False`. + :param bool v_flip: Enable vertical display flip, default: `False`. + :param spi_bus: SPI device. If `None` then a default :class:`spidev.SpiDev` object is used. Default: `None`. + :type spi_bus: :class:`spidev.SpiDev` + :param i2c_bus: SMB object. If `None` then :class:`smbus2.SMBus(1)` is used. + :type i2c_bus: :class:`smbus2.SMBus` + :param gpio: GPIO module. If `None` then `RPi.GPIO` is imported. Default: `None`. + :type gpio: :class:`RPi.GPIO` + """ + 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 = _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: + 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)) + if self.eeprom.display_variant in (1, 6) and self.eeprom.get_color() == 'red': + self.lut = 'red_ht' + + self.buf = numpy.zeros((self.height, self.width), dtype=numpy.uint8) + self.border_colour = 0 + + self.dc_pin = dc_pin + self.reset_pin = reset_pin + self.busy_pin = busy_pin + self.cs_channel = cs_channel + self.h_flip = h_flip + self.v_flip = v_flip + + self._gpio = gpio + self._gpio_setup = False + + """Inky Lookup Tables. + + These lookup tables comprise of two sets of values. + + The first set of values, formatted as binary, describe the voltages applied during the six update phases: + + Phase 0 Phase 1 Phase 2 Phase 3 Phase 4 Phase 5 Phase 6 + A B C D + 0b01001000, 0b10100000, 0b00010000, 0b00010000, 0b00010011, 0b00000000, 0b00000000, LUT0 - Black + 0b01001000, 0b10100000, 0b10000000, 0b00000000, 0b00000011, 0b00000000, 0b00000000, LUT1 - White + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, NOT USED BY HARDWARE + 0b01001000, 0b10100101, 0b00000000, 0b10111011, 0b00000000, 0b00000000, 0b00000000, LUT3 - Yellow or Red + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, LUT4 - VCOM + + There are seven possible phases, arranged horizontally, and only the phases with duration/repeat information + (see below) are used during the update cycle. + + Each phase has four steps: A, B, C and D. Each step is represented by two binary bits and these bits can + have one of four possible values representing the voltages to be applied. The default values follow: + + 0b00: VSS or Ground + 0b01: VSH1 or 15V + 0b10: VSL or -15V + 0b11: VSH2 or 5.4V + + During each phase the Black, White and Yellow (or Red) stages are applied in turn, creating a voltage + differential across each display pixel. This is what moves the physical ink particles in their suspension. + + The second set of values, formatted as hex, describe the duration of each step in a phase, and the number + of times that phase should be repeated: + + Duration Repeat + A B C D + 0x10, 0x04, 0x04, 0x04, 0x04, <-- Timings for Phase 0 + 0x10, 0x04, 0x04, 0x04, 0x04, <-- Timings for Phase 1 + 0x04, 0x08, 0x08, 0x10, 0x10, etc + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + + The duration and repeat parameters allow you to take a single sequence of A, B, C and D voltage values and + transform them into a waveform that - effectively - wiggles the ink particles into the desired position. + + In all of our LUT definitions we use the first and second phases to flash/pulse and clear the display to + mitigate image retention. The flashing effect is actually the ink particles being moved from the bottom to + the top of the display repeatedly in an attempt to reset them back into a sensible resting position. + + """ + self._luts = { + 'black': [ + 0b01001000, 0b10100000, 0b00010000, 0b00010000, 0b00010011, 0b00000000, 0b00000000, + 0b01001000, 0b10100000, 0b10000000, 0b00000000, 0b00000011, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, + 0b01001000, 0b10100101, 0b00000000, 0b10111011, 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, + 0x10, 0x04, 0x04, 0x04, 0x04, + 0x10, 0x04, 0x04, 0x04, 0x04, + 0x04, 0x08, 0x08, 0x10, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + ], + 'red': [ + 0b01001000, 0b10100000, 0b00010000, 0b00010000, 0b00010011, 0b00000000, 0b00000000, + 0b01001000, 0b10100000, 0b10000000, 0b00000000, 0b00000011, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, + 0b01001000, 0b10100101, 0b00000000, 0b10111011, 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, + 0x40, 0x0C, 0x20, 0x0C, 0x06, + 0x10, 0x08, 0x04, 0x04, 0x06, + 0x04, 0x08, 0x08, 0x10, 0x10, + 0x02, 0x02, 0x02, 0x40, 0x20, + 0x02, 0x02, 0x02, 0x02, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00 + ], + 'red_ht': [ + 0b01001000, 0b10100000, 0b00010000, 0b00010000, 0b00010011, 0b00010000, 0b00010000, + 0b01001000, 0b10100000, 0b10000000, 0b00000000, 0b00000011, 0b10000000, 0b10000000, + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, + 0b01001000, 0b10100101, 0b00000000, 0b10111011, 0b00000000, 0b01001000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, + 0x43, 0x0A, 0x1F, 0x0A, 0x04, + 0x10, 0x08, 0x04, 0x04, 0x06, + 0x04, 0x08, 0x08, 0x10, 0x0B, + 0x02, 0x04, 0x04, 0x40, 0x10, + 0x06, 0x06, 0x06, 0x02, 0x02, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00 + ], + 'yellow': [ + 0b11111010, 0b10010100, 0b10001100, 0b11000000, 0b11010000, 0b00000000, 0b00000000, + 0b11111010, 0b10010100, 0b00101100, 0b10000000, 0b11100000, 0b00000000, 0b00000000, + 0b11111010, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, + 0b11111010, 0b10010100, 0b11111000, 0b10000000, 0b01010000, 0b00000000, 0b11001100, + 0b10111111, 0b01011000, 0b11111100, 0b10000000, 0b11010000, 0b00000000, 0b00010001, + 0x40, 0x10, 0x40, 0x10, 0x08, + 0x08, 0x10, 0x04, 0x04, 0x10, + 0x08, 0x08, 0x03, 0x08, 0x20, + 0x08, 0x04, 0x00, 0x00, 0x10, + 0x10, 0x08, 0x08, 0x00, 0x20, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 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_channel) + self._spi_bus.max_speed_hz = 488000 + + self._gpio_setup = True + + self._gpio.output(self.reset_pin, self._gpio.LOW) + time.sleep(0.1) + self._gpio.output(self.reset_pin, self._gpio.HIGH) + time.sleep(0.1) + + self._send_command(0x12) # Soft Reset + self._busy_wait() + + def _busy_wait(self): + """Wait for busy/wait pin.""" + while self._gpio.input(self.busy_pin) != self._gpio.LOW: + time.sleep(0.01) + + def _update(self, buf_a, buf_b, busy_wait=True): + """Update display. + + :param buf_a: Black/White pixels + :param buf_b: Yellow/Red pixels + + """ + self.setup() + + packed_height = list(struct.pack('= 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(ssd1608.DRIVER_CONTROL, [self.rows - 1, (self.rows - 1) >> 8, 0x00]) + # Set dummy line period + self._send_command(ssd1608.WRITE_DUMMY, [0x1B]) + # Set Line Width + self._send_command(ssd1608.WRITE_GATELINE, [0x0B]) + # Data entry squence (scan direction leftward and downward) + self._send_command(ssd1608.DATA_MODE, [0x03]) + # Set ram X start and end position + xposBuf = [0x00, self.cols // 8 - 1] + self._send_command(ssd1608.SET_RAMXPOS, xposBuf) + # Set ram Y start and end position + yposBuf = [0x00, 0x00, (self.rows - 1) & 0xFF, (self.rows - 1) >> 8] + self._send_command(ssd1608.SET_RAMYPOS, yposBuf) + # VCOM Voltage + self._send_command(ssd1608.WRITE_VCOM, [0x70]) + # Write LUT DATA + self._send_command(ssd1608.WRITE_LUT, self._luts[self.lut]) + + if self.border_colour == self.BLACK: + self._send_command(ssd1608.WRITE_BORDER, 0b00000000) + # GS Transition + Waveform 00 + GSA 0 + GSB 0 + elif self.border_colour == self.RED and self.colour == 'red': + self._send_command(ssd1608.WRITE_BORDER, 0b00000110) + # GS Transition + Waveform 01 + GSA 1 + GSB 0 + elif self.border_colour == self.YELLOW and self.colour == 'yellow': + self._send_command(ssd1608.WRITE_BORDER, 0b00001111) + # GS Transition + Waveform 11 + GSA 1 + GSB 1 + elif self.border_colour == self.WHITE: + self._send_command(ssd1608.WRITE_BORDER, 0b00000001) + # GS Transition + Waveform 00 + GSA 0 + GSB 1 + + # Set RAM address to 0, 0 + self._send_command(ssd1608.SET_RAMXCOUNT, [0x00]) + self._send_command(ssd1608.SET_RAMYCOUNT, [0x00, 0x00]) + + for data in ((ssd1608.WRITE_RAM, buf_a), (ssd1608.WRITE_ALTRAM, buf_b)): + cmd, buf = data + self._send_command(cmd, buf) + + self._busy_wait() + self._send_command(ssd1608.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.rows, self.cols)) + canvas.paste(image, (self.offset_x, self.offset_y)) + self.buf = numpy.array(canvas, dtype=numpy.uint8).reshape((self.cols, self.rows)) + + 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) \ No newline at end of file