"""Pure-Python tests for the SSD168x SPI decoder. These tests are the **specification** for the future Velxio frontend emulator. They walk the decoder through the exact byte sequences GxEPD2 and Adafruit_EPD emit and assert the resulting framebuffer is what a real panel would have shown. No QEMU, no DOM, no backend — just bytes in and pixels out. """ from __future__ import annotations from pathlib import Path import sys import pytest _HERE = Path(__file__).parent sys.path.insert(0, str(_HERE)) from ssd168x_decoder import ( # noqa: E402 (sys.path tweak above) SSD168xDecoder, Frame, CMD_SW_RESET, CMD_DRIVER_OUTPUT_CTRL, CMD_DATA_ENTRY_MODE, CMD_SET_RAMX_RANGE, CMD_SET_RAMY_RANGE, CMD_BORDER_WAVEFORM, CMD_DISP_UPDATE_CTRL_1, CMD_TEMP_SENSOR, CMD_SET_RAMX_COUNTER, CMD_SET_RAMY_COUNTER, CMD_WRITE_BLACK_VRAM, CMD_WRITE_RED_VRAM, CMD_DISP_UPDATE_CTRL_2, CMD_MASTER_ACTIVATION, CMD_DEEP_SLEEP, ) # ── Helpers ─────────────────────────────────────────────────────────────────── def cmd(c): """Return one (byte, dc=False) tuple — DC LOW means command.""" return [(c, False)] def data(*bs): """Return n (byte, dc=True) tuples — DC HIGH means data.""" return [(b, True) for b in bs] def feed_all(d, *streams): """Feed a flat sequence of (byte, dc) tuples into the decoder.""" for stream in streams: for byte, dc_high in stream: d.feed(byte, dc_high) def gxepd2_init_154(d): """The init sequence GxEPD2 emits for the GxEPD2_154_D67 (200×200, SSD1681). Lifted from GxEPD2's src/epd/GxEPD2_154_D67.cpp _InitDisplay() and cross-checked against esp-bsp's esp_lcd_ssd1681_commands.h init. """ feed_all( d, cmd(CMD_SW_RESET), cmd(CMD_DRIVER_OUTPUT_CTRL), data(0xC7, 0x00, 0x00), cmd(CMD_DATA_ENTRY_MODE), data(0x03), cmd(CMD_SET_RAMX_RANGE), data(0x00, 0x18), cmd(CMD_SET_RAMY_RANGE), data(0x00, 0x00, 0xC7, 0x00), cmd(CMD_BORDER_WAVEFORM), data(0x05), cmd(CMD_DISP_UPDATE_CTRL_1), data(0x00, 0x80), cmd(CMD_TEMP_SENSOR), data(0x80), cmd(CMD_SET_RAMX_COUNTER), data(0x00), cmd(CMD_SET_RAMY_COUNTER), data(0x00, 0x00), ) # ── Tests ───────────────────────────────────────────────────────────────────── class TestInitSequence: """The decoder must accept GxEPD2's init without complaint.""" def test_clean_init_for_154_panel(self): d = SSD168xDecoder(width=200, height=200) gxepd2_init_154(d) assert d.unknown_cmds == [], ( f"GxEPD2 init emitted unknown cmds: {d.unknown_cmds!r}" ) assert d.refreshed_count == 0, "Init alone must NOT trigger a refresh" assert not d.in_deep_sleep def test_sw_reset_clears_state(self): d = SSD168xDecoder(width=200, height=200) # Pollute the BW plane. feed_all(d, cmd(CMD_WRITE_BLACK_VRAM), data(0x00, 0x00, 0x00)) assert any(b != 0xFF for b in d.bw_ram[:25]) # Reset. feed_all(d, cmd(CMD_SW_RESET)) assert all(b == 0xFF for b in d.bw_ram[:25]), "SW reset must clear BW RAM" class TestRamWindowing: """The X/Y range + entry-mode commands must steer pixel writes correctly.""" def test_ramx_ramy_range_sets_window(self): d = SSD168xDecoder(width=200, height=200) feed_all( d, cmd(CMD_SET_RAMX_RANGE), data(0x00, 0x18), # full width (0..24) cmd(CMD_SET_RAMY_RANGE), data(0x00, 0x00, 0xC7, 0x00), # full height (0..199) ) assert d._xrange == (0x00, 0x18) assert d._yrange == (0x0000, 0x00C7) def test_set_counters_seek_position(self): d = SSD168xDecoder(width=200, height=200) feed_all( d, cmd(CMD_SET_RAMX_COUNTER), data(0x05), cmd(CMD_SET_RAMY_COUNTER), data(0x10, 0x00), ) assert d._x_byte == 0x05 assert d._y == 0x0010 class TestPixelWriting: """Writing to the BW VRAM must land bytes in the right framebuffer position.""" def test_write_black_vram_increments_x(self): d = SSD168xDecoder(width=200, height=200) gxepd2_init_154(d) feed_all(d, cmd(CMD_WRITE_BLACK_VRAM), data(0x00, 0xFF, 0xAA)) assert d.bw_ram[0] == 0x00 assert d.bw_ram[1] == 0xFF assert d.bw_ram[2] == 0xAA assert d._x_byte == 3 assert d._y == 0 def test_write_wraps_to_next_row(self): # Tiny window so we can hit the wrap fast (x range 0..1 = 2 bytes wide) d = SSD168xDecoder(width=200, height=200) feed_all( d, cmd(CMD_DATA_ENTRY_MODE), data(0x03), cmd(CMD_SET_RAMX_RANGE), data(0x00, 0x01), cmd(CMD_SET_RAMX_COUNTER), data(0x00), cmd(CMD_SET_RAMY_COUNTER), data(0x00, 0x00), ) feed_all(d, cmd(CMD_WRITE_BLACK_VRAM), data(0xAA, 0xBB, 0xCC, 0xDD)) # First two bytes go to row 0; next two to row 1. assert d.bw_ram[0] == 0xAA assert d.bw_ram[1] == 0xBB bpr = 25 assert d.bw_ram[bpr + 0] == 0xCC assert d.bw_ram[bpr + 1] == 0xDD class TestFrameLatchAndCompose: """0x20 ACTIVATE must trigger flush; red plane must win over black.""" def test_activate_calls_on_flush(self): seen = [] d = SSD168xDecoder(width=200, height=200, on_flush=lambda f: seen.append(f)) gxepd2_init_154(d) # White everywhere → all bits are already 0xFF (white). feed_all( d, cmd(CMD_DISP_UPDATE_CTRL_2), data(0xF7), cmd(CMD_MASTER_ACTIVATION), ) assert len(seen) == 1 assert d.refreshed_count == 1 frame: Frame = seen[0] assert frame.width == 200 and frame.height == 200 assert all(p == 1 for p in frame.pixels), ( "Default RAM is 0xFF (all bits=1) → all pixels must be white" ) def test_red_plane_wins_over_black(self): d = SSD168xDecoder(width=8, height=2) # tiny 1-byte-wide panel # Black plane: row 0 all-black (0x00), row 1 all-white (0xFF) feed_all(d, cmd(CMD_WRITE_BLACK_VRAM), data(0x00, 0xFF)) # Red plane: row 0 first 4 px red (0xF0), row 1 nothing (0x00) feed_all(d, cmd(CMD_SET_RAMX_COUNTER), data(0x00)) feed_all(d, cmd(CMD_SET_RAMY_COUNTER), data(0x00, 0x00)) feed_all(d, cmd(CMD_WRITE_RED_VRAM), data(0xF0, 0x00)) frame = d.compose_frame() # Row 0 cols 0..3: red (2). Cols 4..7: still black (0). Row 1 all white (1). assert frame.pixels[0:4] == [2, 2, 2, 2] assert frame.pixels[4:8] == [0, 0, 0, 0] assert frame.pixels[8:16] == [1, 1, 1, 1, 1, 1, 1, 1] class TestDeepSleepAndUnknownCmds: """Deep sleep + tolerance for vendor-specific quirks.""" def test_deep_sleep_flag_is_set(self): d = SSD168xDecoder(width=200, height=200) feed_all(d, cmd(CMD_DEEP_SLEEP), data(0x01)) assert d.in_deep_sleep is True def test_unknown_cmd_is_logged_not_raised(self): """Real panel firmware sometimes emits vendor-specific bytes; the decoder logs them so we can audit later, but never raises.""" d = SSD168xDecoder(width=200, height=200) feed_all(d, cmd(0xAB), data(0x01, 0x02)) # 0xAB is undefined assert 0xAB in d.unknown_cmds class TestEndToEndHelloWorld: """A canonical 'init → write white frame with one black pixel → activate' flow reaches the on_flush callback with exactly one black pixel.""" def test_single_black_pixel_at_origin(self): captured = [] d = SSD168xDecoder(width=200, height=200, on_flush=lambda f: captured.append(f)) gxepd2_init_154(d) # Write the BW plane: byte 0 = 0x7F (top-left pixel = 0/black, others = 1/white) feed_all(d, cmd(CMD_SET_RAMX_COUNTER), data(0x00)) feed_all(d, cmd(CMD_SET_RAMY_COUNTER), data(0x00, 0x00)) feed_all(d, cmd(CMD_WRITE_BLACK_VRAM), data(0x7F)) feed_all( d, cmd(CMD_DISP_UPDATE_CTRL_2), data(0xF7), cmd(CMD_MASTER_ACTIVATION), ) assert len(captured) == 1 frame = captured[0] # Top-left pixel should be black (0); pixel (1, 0) should be white (1). assert frame.pixels[0] == 0, "top-left expected black" assert frame.pixels[1] == 1, "pixel (1,0) expected white" if __name__ == "__main__": pytest.main([__file__, "-v"])