Port real epd13in3e driver from vendor code; fix wire-raster stride bug
Vendored the panel's init/LUT/refresh register sequence from three
independent Waveshare reference drivers for this exact panel+controller
(RaspberryPi/c, ESP32, and the ESP32-S3-ePaper-13.3E6 ESP-IDF example),
which all agree byte-for-byte. The epd13in3e.c #error is gone; it
compiles clean and links (verified via /build-firmware ee02).
That vendor code also revealed the panel's SPI wire raster is a native
1200x1600 (portrait), not 1600x1200 as previously assumed -- rotated 90
degrees from the panel's landscape mount/marketing size. The old
assumption wasn't just a rotation bug: 1600x1200 and 1200x1600 don't
share a row stride, so packing at the wrong one would have shredded
images into a repeating diagonal garble on real hardware, not just
displayed them sideways. Fixed with a new PANEL_WIRE_TRANSPOSE in
image_pipeline.py, applied after the existing per-frame
ORIENTATION_TRANSPOSE, with a direction-agnostic regression test that
catches the stride bug specifically (a byte-count check alone can't,
since both orientations pack to the same total size).
A full ee02 build still fails, but no longer because of this driver --
main/{back,next,combo}_button.c call an ESP32-C6-only deep-sleep
GPIO-wakeup API with no ESP32-S3 fallback, a separate pre-existing gap
that was simply hidden behind the panel driver's old #error. See
docs/hardware.md for details; CI's continue-on-error on this board
stays in place until that's fixed too.
This commit is contained in:
@@ -241,3 +241,57 @@ def test_render_placeholder_size_for_the_real_13in3_panel(orientation):
|
||||
panel_w, panel_h = PANEL_SPECS["epd13in3e"]
|
||||
data = render_placeholder(["Not configured yet"], orientation=orientation, panel_type="epd13in3e")
|
||||
assert len(data) == panel_w * panel_h // 2
|
||||
|
||||
|
||||
def test_transpose_and_pack_epd13in3e_uses_true_wire_raster_stride():
|
||||
"""Regression guard for a corruption bug, not just a rotation bug: the
|
||||
13.3" panel's SPI controller addresses a native 1200x1600 raster (600
|
||||
bytes/row x 1600 rows), rotated 90 degrees from PANEL_SPECS's
|
||||
1600x1200 mount/marketing size (800 bytes/row x 1200 rows) -- see
|
||||
PANEL_WIRE_TRANSPOSE's own comment. Both shapes pack to the identical
|
||||
960000-byte total, so a regression here wouldn't fail a plain length
|
||||
assertion -- it would ship a driver that slices real image rows at the
|
||||
wrong byte offsets and shreds the picture on a real panel.
|
||||
|
||||
This probes stride, not rotation direction: a vertical stripe (values
|
||||
constant along the *mount* image's y-axis) stays constant along
|
||||
whichever axis absorbs that constancy under ANY 90-degree-multiple
|
||||
rotation, so this holds regardless of which direction
|
||||
PANEL_WIRE_TRANSPOSE ends up using -- only the true 600-byte wire row
|
||||
stride makes each decoded row uniform; decoding at the wrong (800-byte
|
||||
mount) stride would slice across real row boundaries and mix both
|
||||
colors into every "row"."""
|
||||
from PIL import ImageDraw
|
||||
|
||||
from app.image_pipeline import (
|
||||
DEFAULT_PALETTE_RGB,
|
||||
PANEL_SPECS,
|
||||
_build_palette_image,
|
||||
_transpose_and_pack,
|
||||
)
|
||||
|
||||
mount_w, mount_h = PANEL_SPECS["epd13in3e"] # (1600, 1200)
|
||||
wire_w, wire_h = mount_h, mount_w # (1200, 1600) -- the true SPI wire raster
|
||||
|
||||
img = Image.new("RGB", (mount_w, mount_h), (255, 255, 255))
|
||||
ImageDraw.Draw(img).rectangle([0, 0, mount_w // 2 - 1, mount_h - 1], fill=(0, 0, 0))
|
||||
quantized = img.quantize(palette=_build_palette_image(DEFAULT_PALETTE_RGB))
|
||||
|
||||
packed = _transpose_and_pack(quantized, "landscape", panel_type="epd13in3e")
|
||||
assert len(packed) == wire_w * wire_h // 2
|
||||
|
||||
row_bytes = wire_w // 2 # 600 -- the true wire row stride
|
||||
first_row = packed[0:row_bytes]
|
||||
last_row = packed[(wire_h - 1) * row_bytes: wire_h * row_bytes]
|
||||
|
||||
def nibbles(row_bytes_slice):
|
||||
vals = set()
|
||||
for b in row_bytes_slice:
|
||||
vals.add(b >> 4)
|
||||
vals.add(b & 0x0F)
|
||||
return vals
|
||||
|
||||
first_nibbles, last_nibbles = nibbles(first_row), nibbles(last_row)
|
||||
assert len(first_nibbles) == 1, "first wire row should be a single color at the true 600-byte stride"
|
||||
assert len(last_nibbles) == 1, "last wire row should be a single color at the true 600-byte stride"
|
||||
assert first_nibbles != last_nibbles, "the black/white split should still show up across wire rows"
|
||||
|
||||
Reference in New Issue
Block a user