Add server-side support for a second panel (13.3in Spectra 6 / EE02) and scaffold its firmware target
Build and push server image / test (push) Successful in 45s
Firmware build check / build-check (push) Successful in 2m50s
Build and push server image / build-and-push (push) Successful in 4m36s
Build and push server image / deploy (push) Failing after 1m34s

Server: Frame.panel_type (new column + migration) is auto-derived from
the device's reported board (X-Frame-Board), never user-set -- the
panel is a property of the hardware, not a picker in the UI.
image_pipeline's packing/render pipeline is parameterized by panel
geometry instead of hardcoded 800x480 globals, with the real confirmed
13.3in geometry (1600x1200) registered alongside the original 7.3in
panel. Existing 7.3in frames are unaffected (column default + board
mapping both resolve to the original panel).

Board identifiers are also renamed (devkit/xiao -> devkit_esp32c6/
xiao_esp32c6, plus new "ee02") since the EE02 board also carries a XIAO
module -- "xiao" alone stopped disambiguating hardware. The server
keeps accepting the legacy bare names indefinitely for already-flashed
devices.

Firmware: scaffolds a third build target (ee02, ESP32-S3 -- a real
chip-target change, not just a same-chip Kconfig variant like xiao) and
a new epd13in3e driver component skeleton. The actual panel init/LUT/
refresh register sequence isn't ported from vendor demo code yet (none
was available), so that component deliberately fails to compile
(#error) rather than risk sending unverified register values to real
hardware -- devkit/xiao are unaffected and build identically to before.
CI's ee02 build step is continue-on-error for the same reason.
This commit is contained in:
2026-08-04 20:08:22 +00:00
parent 1d39e439ff
commit 474b92a282
44 changed files with 1287 additions and 172 deletions
@@ -161,3 +161,83 @@ def test_render_panel_backfilled_full_panel_widget_matches_grid_full_panel_rect(
region = Image.new("RGB", px[2:], (10, 20, 30))
data = render_panel([(px, region)], orientation="landscape")
assert len(data) == EXPECTED_BYTES
# --- a second, synthetic panel size (proves the packing path is genuinely
# resolution-agnostic, ahead of the real 13.3" panel's numbers existing --
# see image_pipeline._transpose_and_pack, which derives its output size
# from the quantized image itself rather than a hardcoded EPD_WIDTH/
# EPD_HEIGHT global) ---
@pytest.fixture
def synthetic_panel(monkeypatch):
"""Registers a second PANEL_SPECS entry, a different size than the
real 7.3" panel, without needing the real 13.3" panel's confirmed
resolution to exist yet."""
from app import image_pipeline
monkeypatch.setitem(image_pipeline.PANEL_SPECS, "test_panel", (600, 400))
return "test_panel", 600, 400
@pytest.mark.parametrize("orientation", ORIENTATIONS)
def test_render_panel_size_for_a_synthetic_second_panel_type(synthetic_panel, orientation):
from app.image_pipeline import logical_render_size
panel_type, panel_w, panel_h = synthetic_panel
w, h = logical_render_size(orientation, panel_w, panel_h)
region = Image.new("RGB", (w, h), (200, 0, 0))
data = render_panel([((0, 0, w, h), region)], orientation=orientation, panel_type=panel_type)
assert len(data) == panel_w * panel_h // 2
@pytest.mark.parametrize("orientation", ORIENTATIONS)
def test_render_placeholder_size_for_a_synthetic_second_panel_type(synthetic_panel, orientation):
panel_type, panel_w, panel_h = synthetic_panel
data = render_placeholder(["Not configured yet"], orientation=orientation, panel_type=panel_type)
assert len(data) == panel_w * panel_h // 2
def test_render_panel_default_panel_type_is_unaffected_by_a_new_registry_entry(synthetic_panel):
"""A second PANEL_SPECS entry existing must never change what an
ordinary (no panel_type passed) render produces -- every existing
7.3" frame's output stays byte-identical regardless of what other
panels get registered."""
data = render_placeholder(["Not configured yet"], orientation="landscape")
assert len(data) == EXPECTED_BYTES
def test_panel_size_falls_back_to_the_original_panel_for_unknown_types():
from app.image_pipeline import panel_size
assert panel_size("nonexistent") == (EPD_WIDTH, EPD_HEIGHT)
assert panel_size("") == (EPD_WIDTH, EPD_HEIGHT)
# --- the real (not synthetic) 13.3" Spectra 6 / EE02 panel geometry,
# confirmed from Waveshare's/Seeed's public product pages -- the vendor
# init/LUT/refresh sequence firmware-side is still unconfirmed (see
# image_pipeline.PANEL_SPECS's own comment), but the geometry itself is
# real, not a placeholder, so it gets the same coverage as the 7.3" panel
# rather than just the synthetic-panel tests above. ---
@pytest.mark.parametrize("orientation", ORIENTATIONS)
def test_render_panel_size_for_the_real_13in3_panel(orientation):
from app.image_pipeline import PANEL_SPECS, logical_render_size
panel_w, panel_h = PANEL_SPECS["epd13in3e"]
w, h = logical_render_size(orientation, panel_w, panel_h)
region = Image.new("RGB", (w, h), (200, 0, 0))
data = render_panel([((0, 0, w, h), region)], orientation=orientation, panel_type="epd13in3e")
assert len(data) == panel_w * panel_h // 2
@pytest.mark.parametrize("orientation", ORIENTATIONS)
def test_render_placeholder_size_for_the_real_13in3_panel(orientation):
from app.image_pipeline import PANEL_SPECS
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