diff --git a/adafruit_epd/acep_7color.py b/adafruit_epd/acep_7color.py new file mode 100644 index 0000000..0d6a86a --- /dev/null +++ b/adafruit_epd/acep_7color.py @@ -0,0 +1,170 @@ +# SPDX-FileCopyrightText: 2023 Liz Clark for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_epd.ACEP` - Adafruit ACEP - ePaper display driver +==================================================================================== +CircuitPython driver for Adafruit ACEP display breakouts +* Author(s): Dean Miller +""" + +import time +from micropython import const +import adafruit_framebuf +from adafruit_epd.epd import Adafruit_ADV_EPD + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_EPD.git" + +_ACEP_PANEL_SETTING = const(0x00) +_ACEP_POWER_SETTING = const(0x01) +_ACEP_POWER_OFF = const(0x02) +_ACEP_POWER_OFF_SEQUENCE = const(0x03) +_ACEP_POWER_ON = const(0x04) +_ACEP_BOOSTER_SOFT_START = const(0x06) +_ACEP_DEEP_SLEEP = const(0x07) +_ACEP_DTM = const(0x10) +_ACEP_DISPLAY_REFRESH = const(0x12) +_ACEP_PLL = const(0x30) +_ACEP_TSE = const(0x40) +_ACEP_CDI = const(0x50) +_ACEP_TCON = const(0x60) +_ACEP_RESOLUTION = const(0x61) +_ACEP_PWS = const(0xE3) + + +class Adafruit_ACEP(Adafruit_ADV_EPD): + """driver class for Adafruit ACEP ePaper display breakouts""" + + # pylint: disable=too-many-arguments + def __init__( + self, width, height, spi, *, cs_pin, dc_pin, sramcs_pin, rst_pin, busy_pin + ): + super().__init__( + width, height, spi, cs_pin, dc_pin, sramcs_pin, rst_pin, busy_pin + ) + + if (height % 8) != 0: + height += 8 - (height % 8) + + self._buffer0_size = int(width * height / 2) + self._buffer1_size = self._buffer0_size + self._buffer2_size = self._buffer0_size + self._buffer3_size = self._buffer0_size + self._buffer4_size = self._buffer0_size + self._buffer5_size = self._buffer0_size + + self._buffer0 = bytearray(self._buffer0_size) + self._buffer1 = self._buffer0 + self._buffer2 = self._buffer0 + self._buffer3 = self._buffer0 + self._buffer4 = self._buffer0 + self._buffer5 = self._buffer0 + + self._framebuf_black = adafruit_framebuf.FrameBuffer( + self._buffer0, width, height, buf_format=adafruit_framebuf.MHMSB + ) + self._framebuf_green = adafruit_framebuf.FrameBuffer( + self._buffer1, width, height, buf_format=adafruit_framebuf.MHMSB + ) + self._framebuf_blue = adafruit_framebuf.FrameBuffer( + self._buffer2, width, height, buf_format=adafruit_framebuf.MHMSB + ) + self._framebuf_red = adafruit_framebuf.FrameBuffer( + self._buffer3, width, height, buf_format=adafruit_framebuf.MHMSB + ) + self._framebuf_yellow = adafruit_framebuf.FrameBuffer( + self._buffer4, width, height, buf_format=adafruit_framebuf.MHMSB + ) + self._framebuf_orange = adafruit_framebuf.FrameBuffer( + self._buffer5, width, height, buf_format=adafruit_framebuf.MHMSB + ) + + self.set_black_buffer(0, True) + self.set_green_buffer(0, False) + self.set_blue_buffer(0, False) + self.set_red_buffer(0, False) + self.set_yellow_buffer(0, False) + self.set_orange_buffer(0, False) + # pylint: enable=too-many-arguments + + def begin(self, reset=True): + """Begin communication with the display and set basic settings""" + if reset: + self.hardware_reset() + self.power_down() + + def busy_wait(self): + """Wait for display to be done with current task, either by polling the + busy pin, or pausing""" + if self._busy: + while not self._busy.value: + time.sleep(0.1) + else: + time.sleep(0.5) + + def power_up(self): + """Power up the display in preparation for writing RAM and updating""" + self.hardware_reset() + time.sleep(0.2) + self.busy_wait() + + time.sleep(0.1) + self.command(_ACEP_PANEL_SETTING, bytearray([0xEF, 0x08])) + self.command(_ACEP_POWER_SETTING, bytearray([0x37, 0x00, 0x23, 0x23])) + self.command(_ACEP_POWER_OFF_SEQUENCE, bytearray([0x00])) + self.command(_ACEP_BOOSTER_SOFT_START, bytearray([0xC7, 0xC7, 0x1D])) + self.command(_ACEP_PLL, bytearray([0x3C])) + self.command(_ACEP_TSE, bytearray([0x00])) + self.command(_ACEP_CDI, bytearray([0x37])) + self.command(_ACEP_TCON, bytearray([0x22])) + self.command(_ACEP_RESOLUTION, bytearray([0x02, 0x58, 0x01, 0xC0])) + self.command(_ACEP_PWS, bytearray([0xAA])) + time.sleep(0.1) + self.command(_ACEP_CDI, bytearray([0x37])) + + self.command(_ACEP_RESOLUTION, bytearray([0x02, 0x58, 0x01, 0xC0])) + time.sleep(0.1) + + def power_down(self): + """Power down the display - required when not actively displaying!""" + time.sleep(1) + + self.command(_ACEP_DEEP_SLEEP, bytearray([0xA5])) + + time.sleep(0.1) + + def set_resolution(self): + self.command(_ACEP_RESOLUTION, bytearray([0x02, 0x58, 0x01, 0xC0])) + self.command(_ACEP_DTM, end=False) + + def update(self): + """Update the display from internal memory""" + self.command(_ACEP_POWER_ON) + self.busy_wait() + # self.command(_ACEP_DISPLAY_REFRESH, bytearray([0x01, 0x00])) + self.command(_ACEP_DISPLAY_REFRESH) + self.busy_wait() + self.command(_ACEP_POWER_OFF) + if not self._busy: + time.sleep(15) # wait 15 seconds + else: + self.busy_wait() + time.sleep(0.2) + + def write_ram(self, index): + """Send the one byte command for starting the RAM write process. Returns + the byte read at the same time over SPI. index is the RAM buffer, can be + 0 or 1 for tri-color displays.""" + # self.command(_ACEP_DTM, end=False) + if index == 0: + return self.command(_ACEP_DTM, end=False) + if index == 1: + return self.command(_ACEP_DTM, end=False) + raise RuntimeError("RAM index must be 0 or 1") + + def set_ram_address(self, x, y): # pylint: disable=unused-argument, no-self-use + """Set the RAM address location, not used on this chipset but required by + the superclass""" + return # on this chip it does nothing diff --git a/adafruit_epd/epd.py b/adafruit_epd/epd.py index fcccc26..737a7e7 100644 --- a/adafruit_epd/epd.py +++ b/adafruit_epd/epd.py @@ -10,6 +10,7 @@ """ import time +from PIL import Image from micropython import const from digitalio import Direction from adafruit_epd import mcp_sram @@ -28,6 +29,12 @@ class Adafruit_EPD: # pylint: disable=too-many-instance-attributes, too-many-pu DARK = const(4) LIGHT = const(5) + acep_GREEN = const(2) + acep_BLUE = const(3) + acep_RED = const(4) + acep_YELLOW = const(5) + acep_ORANGE = const(6) + def __init__( self, width, height, spi, cs_pin, dc_pin, sramcs_pin, rst_pin, busy_pin ): # pylint: disable=too-many-arguments @@ -249,11 +256,16 @@ def set_color_buffer(self, index, inverted): def _color_dup(self, func, args, color): black = getattr(self._blackframebuf, func) red = getattr(self._colorframebuf, func) + # pylint: disable=pointless-string-statement if self._blackframebuf is self._colorframebuf: # monochrome black(*args, color=(color != Adafruit_EPD.WHITE) != self._black_inverted) else: black(*args, color=(color == Adafruit_EPD.BLACK) != self._black_inverted) - red(*args, color=(color == Adafruit_EPD.RED) != self._color_inverted) + red( + *args, + color=(color in Adafruit_EPD.acep_RED, Adafruit_EPD.RED) + != self._color_inverted + ) def pixel(self, x, y, color): """draw a single pixel in the display buffer""" @@ -261,9 +273,10 @@ def pixel(self, x, y, color): def fill(self, color): """fill the screen with the passed color""" - red_fill = ((color == Adafruit_EPD.RED) != self._color_inverted) * 0xFF black_fill = ((color == Adafruit_EPD.BLACK) != self._black_inverted) * 0xFF - + red_fill = ( + (color in Adafruit_EPD.acep_RED, Adafruit_EPD.RED) != self._color_inverted + ) * 0xFF if self.sram: self.sram.erase(0x00, self._buffer1_size, black_fill) self.sram.erase(self._buffer1_size, self._buffer2_size, red_fill) @@ -383,3 +396,323 @@ def image(self, image): self.pixel(x, y, Adafruit_EPD.BLACK) else: raise ValueError("Image must be in mode RGB or mode L.") + + +class Adafruit_ADV_EPD: # pylint: disable=too-many-instance-attributes, too-many-public-methods + """Base class for 7 Color EPD displays""" + + ACEP_BLACK = const(0) + ACEP_WHITE = const(1) + ACEP_GREEN = const(2) + ACEP_BLUE = const(3) + ACEP_RED = const(4) + ACEP_YELLOW = const(5) + ACEP_ORANGE = const(6) + + def __init__( + self, width, height, spi, cs_pin, dc_pin, sramcs_pin, rst_pin, busy_pin + ): # pylint: disable=too-many-arguments + self._width = width + self._height = height + + # Setup reset pin, if we have one + self._rst = rst_pin + if rst_pin: + self._rst.direction = Direction.OUTPUT + + # Setup busy pin, if we have one + self._busy = busy_pin + if busy_pin: + self._busy.direction = Direction.INPUT + + # Setup dc pin (required) + self._dc = dc_pin + self._dc.direction = Direction.OUTPUT + self._dc.value = False + + # Setup cs pin (required) + self._cs = cs_pin + self._cs.direction = Direction.OUTPUT + self._cs.value = True + + # SPI interface (required) + self.spi_device = spi + while not self.spi_device.try_lock(): + time.sleep(0.01) + self.spi_device.configure(baudrate=1000000) # 1 Mhz + self.spi_device.unlock() + + self._spibuf = bytearray(1) + self._single_byte_tx = False + + self.sram = None + + # pylint: disable=line-too-long + self._buffer0_size = ( + self._buffer1_size + ) = ( + self._buffer2_size + ) = self._buffer3_size = self._buffer4_size = self._buffer5_size = 0 + self._buffer0 = ( + self._buffer1 + ) = self._buffer2 = self._buffer3 = self._buffer4 = self._buffer5 = None + self._framebuf_black = ( + self._framebuf_green + ) = ( + self._framebuf_blue + ) = self._framebuf_red = self._framebuf_yellow = self._framebuf_orange = None + self._blackframebuf = ( + self._greenframebuf + ) = ( + self._blueframebuf + ) = self._redframebuf = self._yellowframebuf = self._orangeframebuf = None + self._black_inverted = ( + self._green_inverted + ) = ( + self._blue_inverted + ) = self._red_inverted = self._yellow_inverted = self._orange_inverted = True + self.hardware_reset() + + def hardware_reset(self): + """If we have a reset pin, do a hardware reset by toggling it""" + if self._rst: + self._rst.value = False + time.sleep(0.1) + self._rst.value = True + time.sleep(0.1) + + def command(self, cmd, data=None, end=True): + """Send command byte to display.""" + self._cs.value = True + self._dc.value = False + self._cs.value = False + + while not self.spi_device.try_lock(): + time.sleep(0.01) + ret = self._spi_transfer(cmd) + + if data is not None: + self._dc.value = True + self._spi_transfer(data) + if end: + self._cs.value = True + self.spi_device.unlock() + + return ret + + def _spi_transfer(self, data): + """Transfer one byte or bytearray, toggling the cs pin if required by the EPD chipset""" + if isinstance(data, int): # single byte! + self._spibuf[0] = data + + # easy & fast case: array and no twiddling + if not self._single_byte_tx and isinstance(data, bytearray): + self.spi_device.write(data) + return None + + # if its a single byte + if isinstance(data, int): # single byte! + if self._single_byte_tx: + self._cs.value = False + try: + self.spi_device.write_readinto(self._spibuf, self._spibuf) + except NotImplementedError: + self.spi_device.write(self._spibuf) + if self._single_byte_tx: + self._cs.value = True + return self._spibuf[0] + + if isinstance(data, bytearray): + for x in data: + self._spi_transfer(x) + return None + + def power_up(self): + """Power up the display in preparation for writing RAM and updating. + must be implemented in subclass""" + raise NotImplementedError() + + def power_down(self): + """Power down the display, must be implemented in subclass""" + raise NotImplementedError() + + def update(self): + """Update the display from internal memory, must be implemented in subclass""" + raise NotImplementedError() + + def write_ram(self, index): + """Send the one byte command for starting the RAM write process. Returns + the byte read at the same time over SPI. index is the RAM buffer, can be + 0 or 1 for tri-color displays. must be implemented in subclass""" + raise NotImplementedError() + + def set_ram_address(self, x, y): + """Set the RAM address location, must be implemented in subclass""" + raise NotImplementedError() + + def set_black_buffer(self, index, inverted): + """Set the index for the black buffer data (0 or 1) and whether its inverted""" + if index == 0: + self._blackframebuf = self._framebuf_black + else: + raise RuntimeError("Buffer index must be 0") + self._black_inverted = inverted + + def set_green_buffer(self, index, inverted): + """Set the index for the color buffer data (0 or 1) and whether its inverted""" + if index == 0: + self._greenframebuf = self._framebuf_green + else: + raise RuntimeError("Buffer index must be 0") + self._green_inverted = inverted + + def set_blue_buffer(self, index, inverted): + """Set the index for the color buffer data (0 or 1) and whether its inverted""" + if index == 0: + self._blueframebuf = self._framebuf_blue + else: + raise RuntimeError("Buffer index must be 0") + self._blue_inverted = inverted + + def set_red_buffer(self, index, inverted): + """Set the index for the color buffer data (0 or 1) and whether its inverted""" + if index == 0: + self._redframebuf = self._framebuf_red + else: + raise RuntimeError("Buffer index must be 0") + self._red_inverted = inverted + + def set_yellow_buffer(self, index, inverted): + """Set the index for the color buffer data (0 or 1) and whether its inverted""" + if index == 0: + self._yellowframebuf = self._framebuf_yellow + else: + raise RuntimeError("Buffer index must be 0") + self._yellow_inverted = inverted + + def set_orange_buffer(self, index, inverted): + """Set the index for the color buffer data (0 or 1) and whether its inverted""" + if index == 0: + self._orangeframebuf = self._framebuf_orange + else: + raise RuntimeError("Buffer index must be 0") + self._orange_inverted = inverted + + def color_map(self, image): + # pylint: disable=line-too-long + palette = Image.new("P", (1, 1)) + palette.putpalette((0, 0, 0, 255, 255, 255, 0, 255, 0, 0, 0, 255, 255, 0, 0, 255, 255, 0, 255, 128, 0, ) + (0, 0, 0) * 249) + + img_7color = image.convert("RGB").quantize(palette=palette) + buf_7color = bytearray(img_7color.tobytes("raw")) + + buf = [0x00] * int(self._width * self._height / 2) + count = 0 + for i in range(0, len(buf_7color), 2): + buf[count] = (buf_7color[i] << 4) + buf_7color[i + 1] + count += 1 + + return buf + + def write_to_buffer(self, buffer): + while not self.spi_device.try_lock(): + time.sleep(0.01) + self._dc.value = True + + self._spi_transfer(buffer) + + self._cs.value = True + self.spi_device.unlock() + time.sleep(0.002) + + def display(self): # pylint: disable=too-many-branches + """show the contents of the display buffer""" + self.power_up() + + self.set_ram_address(0, 0) + + self.set_resolution() + # pylint: disable=pointless-string-statement + """while not self.spi_device.try_lock(): + time.sleep(0.01) + self._dc.value = True + + self._spi_transfer(self._buffer1) + + self._cs.value = True + self.spi_device.unlock() + time.sleep(0.002)""" + self.write_to_buffer(self._buffer0) + self.write_to_buffer(self._buffer1) + self.write_to_buffer(self._buffer2) + self.write_to_buffer(self._buffer3) + self.write_to_buffer(self._buffer4) + self.write_to_buffer(self._buffer5) + + self.update() + + def fill(self, color): + """fill the screen with the passed color""" + + black_fill = color == Adafruit_ADV_EPD.ACEP_BLACK + green_fill = color == Adafruit_ADV_EPD.ACEP_GREEN + blue_fill = color == Adafruit_ADV_EPD.ACEP_BLUE + red_fill = color == Adafruit_ADV_EPD.ACEP_RED + yellow_fill = color == Adafruit_ADV_EPD.ACEP_YELLOW + orange_fill = color == Adafruit_ADV_EPD.ACEP_ORANGE + + self._buffer0 = self._framebuf_black.fill(black_fill) + self._buffer1 = self._framebuf_green.fill(green_fill) + self._buffer2 = self._framebuf_blue.fill(blue_fill) + self._buffer3 = self._framebuf_red.fill(red_fill) + self._buffer4 = self._framebuf_yellow.fill(yellow_fill) + self._buffer5 = self._framebuf_orange.fill(orange_fill) + + def pixel(self, x, y, color): + """draw a single pixel in the display buffer""" + self._framebuf1.set_pixel(x, y, color) + + def rect(self, x, y, width, height, color): # pylint: disable=too-many-arguments + """draw a rectangle""" + self._framebuf1.rect(x, y, width, height, color) + + def fill_rect( + self, x, y, width, height, color + ): # pylint: disable=too-many-arguments + """fill a rectangle with the passed color""" + self._buffer0 = self._framebuf_black.fill_rect(x, y, width, height, color) + self._buffer1 = self._framebuf_green.fill_rect(x, y, width, height, color) + self._buffer2 = self._framebuf_blue.fill_rect(x, y, width, height, color) + self._buffer3 = self._framebuf_red.fill_rect(x, y, width, height, color) + self._buffer4 = self._framebuf_yellow.fill_rect(x, y, width, height, color) + self._buffer5 = self._framebuf_orange.fill_rect(x, y, width, height, color) + + # pylint: disable=pointless-string-statement + '''@property + def width(self): + """The width of the display, accounting for rotation""" + if self.rotation in (0, 2): + return self._width + return self._height + + @property + def height(self): + """The height of the display, accounting for rotation""" + if self.rotation in (0, 2): + return self._height + return self._width + + @property + def rotation(self): + """The rotation of the display, can be one of (0, 1, 2, 3)""" + return self._framebuf1.rotation + + @rotation.setter + def rotation(self, val): + self._framebuf1.rotation = val + if self._framebuf2: + self._framebuf2.rotation = val''' + + def set_border(self, color): + """Set the border colour.""" + self.command(0x3C, bytearray([color])) diff --git a/examples/acep_colorsquares_test.py b/examples/acep_colorsquares_test.py new file mode 100644 index 0000000..0230d3e --- /dev/null +++ b/examples/acep_colorsquares_test.py @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries +# SPDX-License-Identifier: MIT + +import digitalio +import busio +import board +from adafruit_epd.epd import Adafruit_ADV_EPD +from adafruit_epd.acep_7color import Adafruit_ACEP + +# create the spi device and pins we will need +spi = busio.SPI(board.EPD_SCK, MOSI=board.EPD_MOSI, MISO=None) +epd_cs = digitalio.DigitalInOut(board.EPD_CS) +epd_dc = digitalio.DigitalInOut(board.EPD_DC) +epd_reset = digitalio.DigitalInOut(board.EPD_RESET) +epd_busy = digitalio.DigitalInOut(board.EPD_BUSY) +srcs = None + +display = Adafruit_ACEP( + 600, + 448, + spi, + cs_pin=epd_cs, + dc_pin=epd_dc, + sramcs_pin=srcs, + rst_pin=epd_reset, + busy_pin=epd_busy, +) + +display.fill(Adafruit_ADV_EPD.ACEP_WHITE) + +display.fill_rect(30, 20, 20, 20, Adafruit_ADV_EPD.ACEP_BLACK) +display.fill_rect(70, 10, 20, 20, Adafruit_ADV_EPD.ACEP_BLUE) +display.fill_rect(110, 10, 20, 20, Adafruit_ADV_EPD.ACEP_RED) +display.fill_rect(150, 10, 20, 20, Adafruit_ADV_EPD.ACEP_GREEN) +display.fill_rect(190, 10, 20, 20, Adafruit_ADV_EPD.ACEP_YELLOW) +display.fill_rect(230, 10, 20, 20, Adafruit_ADV_EPD.ACEP_ORANGE) + +display.set_border(Adafruit_ADV_EPD.ACEP_BLACK) +display.display()