Files
2026-06-21 21:14:18 -04:00

200 lines
7.1 KiB
Python

"""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)