"""CircuitPython ST7305/RLCD driver for Waveshare ESP32-S3-RLCD-4.2. This is a direct migration attempt from the project's MicroPython rlcd.py. It intentionally does not use displayio's display bus because the panel uses a non-standard 2x4 packed 1-bit memory layout. """ import time try: import adafruit_framebuf except ImportError: # CircuitPython bundle dependency. adafruit_framebuf = None class RLCD: WIDTH = 400 HEIGHT = 300 BUFFER_SIZE = (WIDTH * HEIGHT) // 8 def __init__(self, spi, cs, dc, rst, width=WIDTH, height=HEIGHT): if adafruit_framebuf is None: raise RuntimeError("adafruit_framebuf is required in CIRCUITPY/lib") self.spi = spi self.cs = cs self.dc = dc self.rst = rst self.width = width self.height = height self.hw_buffer = bytearray((width * height) // 8) self.canvas_buffer = bytearray((width * height) // 8) self.canvas = adafruit_framebuf.FrameBuffer( self.canvas_buffer, width, height, adafruit_framebuf.MHMSB, ) self.cs.switch_to_output(value=True) self.dc.switch_to_output(value=False) self.rst.switch_to_output(value=True) self.reset() self.init_display() def reset(self): self.rst.value = True time.sleep(0.05) self.rst.value = False time.sleep(0.02) self.rst.value = True time.sleep(0.05) def _lock_spi(self): while not self.spi.try_lock(): pass # CircuitPython SPI settings are only guaranteed while the bus is # locked, so configure after every lock instead of only at startup. self.spi.configure(baudrate=20000000, phase=0, polarity=0) def _unlock_spi(self): self.spi.unlock() def write_cmd(self, cmd): self._lock_spi() try: self.cs.value = False self.dc.value = False self.spi.write(bytes([cmd & 0xFF])) self.cs.value = True finally: self._unlock_spi() def write_data(self, data): if isinstance(data, int): payload = bytes([data & 0xFF]) elif isinstance(data, (bytes, bytearray, memoryview)): payload = data else: payload = bytes(data) self._lock_spi() try: self.cs.value = False self.dc.value = True self.spi.write(payload) self.cs.value = True finally: self._unlock_spi() def init_display(self): # Ported from MicroPython rlcd.py. The old file had one decimal 19 in # the 0xC5 payload; this port uses 0x19 consistently. self.write_cmd(0xD6); self.write_data([0x17, 0x02]) self.write_cmd(0xD1); self.write_data(0x01) self.write_cmd(0xC0); self.write_data([0x11, 0x04]) self.write_cmd(0xC1); self.write_data([0x41, 0x41, 0x41, 0x41]) self.write_cmd(0xC2); self.write_data([0x19, 0x19, 0x19, 0x19]) self.write_cmd(0xC4); self.write_data([0x41, 0x41, 0x41, 0x41]) self.write_cmd(0xC5); self.write_data([0x19, 0x19, 0x19, 0x19]) self.write_cmd(0xD8); self.write_data([0xA6, 0xE9]) self.write_cmd(0xB2); self.write_data(0x05) self.write_cmd(0xB3); self.write_data([0xE5, 0xF6, 0x05, 0x46, 0x77, 0x77, 0x77, 0x77, 0x76, 0x45]) self.write_cmd(0xB4); self.write_data([0x05, 0x46, 0x77, 0x77, 0x77, 0x77, 0x76, 0x45]) self.write_cmd(0x62); self.write_data([0x32, 0x03, 0x1F]) self.write_cmd(0xB7); self.write_data(0x13) self.write_cmd(0xB0); self.write_data(0x64) self.write_cmd(0x11) time.sleep(0.2) self.write_cmd(0xC9); self.write_data(0x00) self.write_cmd(0x36); self.write_data(0x48) self.write_cmd(0x3A); self.write_data(0x11) self.write_cmd(0xB9); self.write_data(0x20) self.write_cmd(0xB8); self.write_data(0x29) self.write_cmd(0x21) self.write_cmd(0x2A); self.write_data([0x12, 0x2A]) self.write_cmd(0x2B); self.write_data([0x00, 0xC7]) self.write_cmd(0x35); self.write_data(0x00) self.write_cmd(0xD0); self.write_data(0xFF) self.write_cmd(0x38) self.write_cmd(0x29) def clear(self, color=0): self.canvas.fill(1 if color else 0) def pixel(self, x, y, color): self.canvas.pixel(x, y, 1 if color else 0) def line(self, x0, y0, x1, y1, color): self.canvas.line(x0, y0, x1, y1, 1 if color else 0) def rect(self, x, y, width, height, color): self.canvas.rect(x, y, width, height, 1 if color else 0) def fill_rect(self, x, y, width, height, color): self.canvas.fill_rect(x, y, width, height, 1 if color else 0) def text(self, text, x, y, color=1): self.canvas.text(str(text), x, y, 1 if color else 0) def text_large(self, text, x, y, scale=2, color=1): # Simple nearest-neighbor scaled 8x8 font rendering using a temp buffer. tmp = bytearray(8) fb = adafruit_framebuf.FrameBuffer(tmp, 8, 8, adafruit_framebuf.MHMSB) color = 1 if color else 0 for ch in str(text): fb.fill(0) fb.text(ch, 0, 0, 1) for py in range(8): for px in range(8): if fb.pixel(px, py): self.canvas.fill_rect(x + px * scale, y + py * scale, scale, scale, color) x += 8 * scale def draw_bitmap_threshold(self, bitmap, x=0, y=0, threshold=1): """Copy any indexable bitmap-like object to the 1-bit canvas. gifio/displayio bitmaps return palette indexes. Treat index 0 as white and anything >= threshold as black by default. """ width = min(getattr(bitmap, "width", self.width), self.width - x) height = min(getattr(bitmap, "height", self.height), self.height - y) for yy in range(height): for xx in range(width): self.canvas.pixel(x + xx, y + yy, 1 if bitmap[xx, yy] >= threshold else 0) def pack(self): """Convert standard horizontal 1-bit canvas into the RLCD 2x4 layout.""" buf = self.hw_buffer for i in range(len(buf)): buf[i] = 0 # Correctness-first port of MicroPython native loop. height = self.height for y in range(height): inv_y = height - 1 - y block_y = inv_y // 4 local_y = inv_y & 0x03 local_y_shift = local_y * 2 row_offset = block_y for x in range(self.width): if self.canvas.pixel(x, y): byte_x = x >> 1 index = byte_x * 75 + row_offset local_x = x & 0x01 bit = 7 - (local_y_shift + local_x) buf[index] |= 1 << bit return buf def show(self): self.pack() self.write_cmd(0x2A) self.write_data([0x12, 0x2A]) self.write_cmd(0x2B) self.write_data([0x00, 0xC7]) self.write_cmd(0x2C) self.write_data(self.hw_buffer) def invert(self, enable=True): self.write_cmd(0x21 if enable else 0x20)