From 2d0adb04a172c504e917a04ae3e13909971b65ac Mon Sep 17 00:00:00 2001 From: mcarr823 <136939846+mcarr823@users.noreply.github.com> Date: Wed, 24 Jan 2024 16:05:27 +1100 Subject: [PATCH] Initial support for EPD2in13v4 --- papertty/drivers/drivers_partial.py | 294 ++++++++++++++++++++++++++++ papertty/papertty.py | 3 +- 2 files changed, 296 insertions(+), 1 deletion(-) diff --git a/papertty/drivers/drivers_partial.py b/papertty/drivers/drivers_partial.py index 5997e04..f04e785 100644 --- a/papertty/drivers/drivers_partial.py +++ b/papertty/drivers/drivers_partial.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . from papertty.drivers.drivers_base import WaveshareEPD +from papertty.drivers.drivers_base import GPIO class WavesharePartial(WaveshareEPD): @@ -594,3 +595,296 @@ def draw(self, x, y, image): self.display_partial(self.get_frame_buffer(image), x, y, x + image.width, x + image.height) else: self.display_full(self.get_frame_buffer(image)) + +class EPD2in13v4(WavesharePartial): + + # Adapted from + # https://github.com/waveshareteam/e-Paper/blob/master/RaspberryPi_JetsonNano/python/lib/waveshare_epd/epd2in13_V4.py + + def __init__(self): + # the actual pixel width is 122, but 128 is the 'logical' width + super().__init__(name='2.13" BW V4', width=128, height=250) + self.cached_buffer = None + self.supports_partial = True + + def reset(self): + self.digital_write(self.RST_PIN, GPIO.HIGH) + self.delay_ms(20) + self.digital_write(self.RST_PIN, GPIO.LOW) + self.delay_ms(2) + self.digital_write(self.RST_PIN, GPIO.HIGH) + self.delay_ms(20) + + def send_command(self, command): + self.digital_write(self.CS_PIN, GPIO.LOW) + super().send_command(command) + self.digital_write(self.CS_PIN, GPIO.HIGH) + + def send_data(self, data): + self.digital_write(self.CS_PIN, GPIO.LOW) + super().send_data(data) + self.digital_write(self.CS_PIN, GPIO.HIGH) + + def send_data_multi(self, data): + self.digital_write(self.CS_PIN, GPIO.LOW) + super().send_data_multi(data) + self.digital_write(self.CS_PIN, GPIO.HIGH) + + def wait_until_idle(self): + while self.digital_read(self.BUSY_PIN) == 1: # 0: idle, 1: busy + self.delay_ms(10) + + def turn_on_display(self): + self.send_command(self.DISPLAY_UPDATE_CONTROL_2) + self.send_data(0xf7) + self.send_command(self.MASTER_ACTIVATION) + self.wait_until_idle() + + def turn_on_display_fast(self): + self.send_command(self.DISPLAY_UPDATE_CONTROL_2) + self.send_data(0xC7) + self.send_command(self.MASTER_ACTIVATION) + self.wait_until_idle() + + def turn_on_display_part(self): + self.send_command(self.DISPLAY_UPDATE_CONTROL_2) + self.send_data(0xff) + self.send_command(self.MASTER_ACTIVATION) + self.wait_until_idle() + + def set_memory_area(self, x_start, y_start, x_end, y_end): + self.send_command(self.SET_RAM_X_ADDRESS_START_END_POSITION) + # x point must be the multiple of 8 or the last 3 bits will be ignored + self.send_data_multi([ + (x_start>>3) & 0xFF, + (x_end>>3) & 0xFF + ]) + + self.send_command(self.SET_RAM_Y_ADDRESS_START_END_POSITION) + self.send_data_multi([ + y_start & 0xFF, + (y_start >> 8) & 0xFF, + y_end & 0xFF, + (y_end >> 8) & 0xFF + ]) + + def set_memory_pointer(self, x, y): + self.send_command(self.SET_RAM_X_ADDRESS_COUNTER) + + # x point must be the multiple of 8 or the last 3 bits will be ignored + self.send_data(x & 0xFF) + + self.send_command(self.SET_RAM_Y_ADDRESS_COUNTER) + self.send_data_multi([ + y & 0xFF, + (y >> 8) & 0xFF + ]) + + def init(self, partial=True, **kwargs): + + self.partial_refresh = partial + + if self.epd_init() != 0: + return -1 + + self.reset() + + self.wait_until_idle() + self.send_command(self.SW_RESET) + self.wait_until_idle() + + self.send_command(self.DRIVER_OUTPUT_CONTROL) + self.send_data_multi([0xf9,0x00,0x00]) + + self.send_command(self.DATA_ENTRY_MODE_SETTING) + self.send_data(0x03) + + self.set_memory_area(0, 0, self.width-1, self.height-1) + self.set_memory_pointer(0, 0) + + self.send_command(0x3c) + self.send_data(0x05) + + self.send_command(self.DISPLAY_UPDATE_CONTROL_1) + self.send_data_multi([0x00,0x80]) + + self.send_command(0x18) + self.send_data(0x80) + + self.wait_until_idle() + + return 0 + + def init_fast(self, partial=True, **kwargs): + + self.partial_refresh = partial + + if self.epd_init() != 0: + return -1 + + self.reset() + + self.send_command(self.SW_RESET) + self.wait_until_idle() + + self.send_command(0x18) # Read built-in temperature sensor + # The below is send_command instead of send_data in the waveshare + # examples, but I think that was a typo. + self.send_data(0x80) + + self.send_command(self.DATA_ENTRY_MODE_SETTING) + self.send_data(0x03) + + self.set_memory_area(0, 0, self.width-1, self.height-1) + self.set_memory_pointer(0, 0) + + self.send_command(0x22) # Load temperature value + self.send_data(0xB1) + self.send_command(0x20) + self.wait_until_idle() + + self.send_command(0x1A) # Write to temperature register + self.send_data_multi([0x64,0x00]) + + self.send_command(0x22) # Load temperature value + self.send_data(0x91) + self.send_command(0x20) + self.wait_until_idle() + + return 0 + + def display_full(self, frame_buffer): + self.send_command(self.WRITE_RAM) + self.send_data_multi(frame_buffer) + self.turn_on_display() + + def display_fast(self, frame_buffer): + self.send_command(self.WRITE_RAM) + self.send_data_multi(frame_buffer) + self.turn_on_display_fast() + + def display_partial(self, frame_buffer, x_start, y_start, x_end, y_end): + self.digital_write(self.RST_PIN, GPIO.LOW) + self.delay_ms(1) + self.digital_write(self.RST_PIN, GPIO.HIGH) + + self.send_command(0x3C) # BorderWavefrom + self.send_data(0x80) + + self.send_command(self.DRIVER_OUTPUT_CONTROL) # Driver output control + self.send_data_multi([0xF9,0x00,0x00]) + + self.send_command(0x11) # data entry mode + self.send_data(0x03) + + # Currently, `draw` always sets the start values to 0, and the + # end values to the panel's full size. + # This matches the waveshare docs/examples, but I suspect that + # it's wrong. + # After all, if it's always full-screen, how is the panel supposed + # to know which part of the display has changed for partial refresh? + # + # So what's currently in place works and matches the docs, and + # attempts to do it differently all failed. + # But I'm guessing there's a better way to do this. + # So if you want to make this panel's partial refresh faster, I'm + # thinking this might be part of the equation. + self.set_memory_area(x_start, y_start, x_end - 1, y_end - 1) + self.set_memory_pointer(x_start, y_start) + + self.send_command(self.WRITE_RAM) + self.send_data_multi(frame_buffer) + + self.turn_on_display_part() + + def displayPartBaseImage(self, frame_buffer): + + # Write the "base" image to the panel, to be used for partial + # refreshes. + # This panel's partial refresh seems to work by writing a base + # image, then updating it each time you draw. + + self.send_command(self.WRITE_RAM) + self.send_data_multi(frame_buffer) + self.send_command(0x26) + self.send_data_multi(frame_buffer) + self.turn_on_display() + + def clear(self): + self.send_command(self.WRITE_RAM) + self.send_data_multi([0xFF] * int(self.height * self.width//8)) + self.turn_on_display() + + def get_frame_buffer(self, image): + # Convert the image to a byte array. + # This function assumes the image is black and white. + # ie. Image mode '1' + # If you try to use it with a grayscale image, it may not work. + return bytearray(image.tobytes('raw')) + + def draw(self, x, y, image): + """Replace a particular area on the display with an image""" + + # Partial refresh works a bit differently for this panel. + # In spite of being a partial refresh, the image still needs + # to be a full-screen image, and we still need to send all of + # the bytes across to the panel. + # So in terms of data transmission, it's no faster than a full refresh. + # + # However, the refresh itself is much faster, and it relies on + # the data written to 0x26 via displayPartBaseImage in order to + # work properly. + # + # I'm sure there's some way to improve this, but after much + # experimenting this is the best I've got right now. + + + # First off, check if we have already written an image before. + # If not, build a buffer (self.cached_buffer) in memory. + # We do this for 2 reasons. + # + # First, because this panel requires a full-screen image each time. + # PaperTTY will send images which aren't the exact dimensions + # of this screen, even if partial is turned off, due to banding. + # So building a full-screen image here for the initial buffer works + # around that, and also means the code works with partial refreshes. + # + # Second, for speed. + # By reusing the buffer from the previous frame we can just overwrite + # the parts which have changed. + # eg. If the buffer is 128x250, and we get a new draw for an image which + # is 64x64, then we can reuse most of the already processed image and only + # replace that small bit which changed. + if self.cached_buffer is None: + self.cached_buffer = [0xFF] * int(self.height * self.width//8) + + # If partial refresh is enabled, write the initial image to the + # appropriate register. + # This is optional for full refreshes, but it is required for + # partial refresh. + if self.partial_refresh: + self.displayPartBaseImage(self.cached_buffer) + + # Now build the new buffer. + # We do this by converting the image to a byte array, then replacing + # the bytes in the cache with the bytes from the new array in the + # appropriate positions. + # This isn't as "pretty" as pasting the image over the old one and + # converting the whole thing, but it is more efficient. + new_buffer = self.get_frame_buffer(image) + width = image.width + width_bytes = width // 8 + panel_width_bytes = self.width // 8 + height = image.height + for h in range(0, height): + src_start = h * width_bytes + src_end = src_start + width_bytes + dst_start = (y+h) * panel_width_bytes + (x // 8) + dst_end = dst_start + width_bytes + self.cached_buffer[dst_start:dst_end] = new_buffer[src_start:src_end] + + # Finally, draw the image. + if self.partial_refresh: + self.display_partial(self.cached_buffer, 0, 0, self.width, self.height) + else: + self.display_full(self.cached_buffer) diff --git a/papertty/papertty.py b/papertty/papertty.py index c016142..dc5d932 100755 --- a/papertty/papertty.py +++ b/papertty/papertty.py @@ -1065,7 +1065,8 @@ def get_drivers(): Format: { '': { 'desc': '', 'class': }, ... }""" driverdict = {} driverlist = [drivers_partial.EPD1in54, drivers_partial.EPD2in13, - drivers_partial.EPD2in13v2, drivers_partial.EPD2in9, + drivers_partial.EPD2in13v2, drivers_partial.EPD2in13v4, + drivers_partial.EPD2in9, drivers_partial.EPD2in13d, driver_4in2.EPD4in2, drivers_full.EPD2in7, drivers_full.EPD3in7, drivers_full.EPD7in5,