# gxepd2 capture harness Host-side tooling that **generates a panel descriptor by observing a real GxEPD2 driver run**, instead of hand-writing one. See ../../CONTRIBUTING.md for the full model; this is the how-to. The vendored GxEPD2 source it compiles against lives in `third_party/GxEPD2/`. Everything here runs on a dev machine with plain `g++` — no ESP toolchain. ## Emit a descriptor One command (from the `code/` root) resolves the driver, builds+runs the harness, and prints a candidate descriptor: ```sh tools/capture.py --list # capturable driver classes tools/capture.py GxEPD2_154_Z90c # candidate -> stdout tools/capture.py GxEPD2_154_Z90c -o /tmp/panel_154_z90c.h tools/capture.py GxEPD2_154_Z90c --debug # also dump the capture structure ``` It finds the driver in `catalog.json` (or by globbing `third_party/GxEPD2/` for one the catalog doesn't list), so there's no build line to hand-write. Under the hood it does exactly: ```sh VSRC=third_party/GxEPD2/src g++ -std=c++17 -I tools/gxepd2_harness/spy -I $VSRC -I tools/gxepd2_harness \ .cpp \ $VSRC/GxEPD2_EPD.cpp $VSRC//GxEPD2_.cpp \ -o /tmp/emit_ ``` `emit_descriptor.h` phase-segments the captured command stream into the init / window / refresh sequences + plane-write command(s), reads resolution / busy-polarity / timeout from the driver, and derives `plane_invert` and 1bpp-vs-packed from a fill probe. For a **4-colour** (`epd4c/`) panel it goes further: `emit_palette_4c.h` drives the driver's own `GxEPD2_4C` wrapper, reads the wire code for each named colour, validates it against the engine's `GXEPD2_PALETTE_4C`, and fills `bits_per_pixel` / `palette` / `blank_code` automatically (an unexpected code map stays a TODO carrying the observed table). A **7-colour** (`epd7c/`) panel works the same way via `emit_palette_7c.h` / the driver's `GxEPD2_7C` wrapper, validated against `GXEPD2_PALETTE_7C` (black/white/green/blue/red/yellow/orange, no code collisions); a panel with a non-standard 7-colour map (e.g. GDEP073E01's rotated codes) stays an explicit TODO with the observed table rather than being mislabelled. For a panel with **no colour wrapper at all** (still `GxEPD2_BW`/binary) that nonetheless packs >1 bit/pixel on the wire - e.g. GxEPD2_583/UC8159c's `_send8pixel`, which expands each 1bpp buffer byte into a wider wire format using only two of its codes - `emit_descriptor.h`'s `make_resolver_bw_packed` is the fallback resolver (used whenever no colour-wrapper resolver applies): it drives the plain driver's own `writeImage` with a uniform all-white/all-black 1bpp buffer directly (no wrapper to instantiate), and if both wire bursts are internally uniform, the byte-count ratio against the pixel count gives `bits_per_pixel` and each fill's byte gives that colour's code - no source reading needed, and no "two 1bpp planes" primitive required from the engine. A driver whose packing depends on more than one bit at a time (dithering, error diffusion) fails the uniformity check and stays an honest TODO. A **dual-region** panel (per-region select + window, e.g. GDEY0579F51 / HX8717) is segmented too: its per-region init / window / refresh, `plane_width` (decoded from the region window-set) and a candidate `line_order` (`PAIRED_EDGES` when a region's burst is 2x its pixel bytes) all come out, with only the exact paired-edges row order left for the diag native-path pass to confirm. Review the result, add any packed-format fields still left as TODO, then save it as `components/gxepd2/panel_.h`. `capture.py` also prints the exact `PANELS` line to paste into `display.py` (key / descriptor macro / format filled from the capture; `part` / `controller` scraped from the driver header to sanity-check, `status` starting at `structural`). `EMIT_DEBUG=1 /tmp/emit_` dumps the image-stage command/burst structure. ## Regression check (CI) `tools/check_descriptors.py` re-emits every shipped panel from its GxEPD2 driver and structurally diffs the result against the committed `panel_*.h` (command sequences + plane layout only, not the hand/diag-filled fields). It exits non-zero on any unexpected mismatch, so wire it into CI as a plain step: ```sh tools/check_descriptors.py # all panels; -v shows the diff on a mismatch ``` A `DIFF` means either an emitter regression or **GxEPD2 upstream drift** (a vendor bump changed that driver — review the panel). Any panel the emitter can't yet segment is listed in its `KNOWN_DIFF` and reported as `XFAIL` (none at present — the dual-region GDEY0579F51 was promoted out once the emitter modelled the region-select layer; an `XPASS` means a `KNOWN_DIFF` entry now reproduces and should be removed). Panels with no GxEPD2 driver source of their own (a synthetic model) are skipped. Run it after any `third_party/GxEPD2/` update. ## Other programs - `tools/capture.py MODEL --sweep` / `--diag` — runs `run_harness.h`'s window-encoder sweep, or `run_diag_harness.h`'s Pattern-A row/byte-order fill, for any capturable model with no `run_.cpp` to hand-write (both headers are already templates over `Driver`; `capture.py` builds a one-off generic `main()` the same way it does for descriptor emission). Older panels still have a committed `run_[_diag].cpp` from before this existed - either form works, prefer `--sweep`/`--diag` for anything new. Output is the full byte-by-byte trace (large for a big/packed panel - redirect to a file and grep for `>>> SWEEP` / `[row-tag]` rather than reading it raw). - `tools/capture.py MODEL --power` — prints just `power_off_sequence`/ `hibernate_sequence` (array + `EpdCommand` table + the two descriptor-field lines to paste), for splicing into an EXISTING `panel_*.h` that shipped before these fields existed, without regenerating - and losing any hand-written commentary in - the rest of the file. Same noisy-trace caveat as `--sweep`/`--diag`: redirect to a file and take the tail (everything after the last `digitalRead` line is the actual result). - `run__probe.cpp` + `probe_plane_invert.h` — print the `plane_invert` probe result on its own. - `run_*diag*.cpp` — the diagnostic-fill passes that derive pixel packing, palette codes, and row order for packed / multi-region panels. Pattern A (`run_diag_harness.h`) only covers the 1bpp byte-preserving path (see CONTRIBUTING.md); Pattern B (`run_diagB_harness.h`) drives a driver's own `GxEPD2_4C`/`GxEPD2_7C` wrapper and its named colours, so it doesn't apply to a still-monochrome (`GxEPD2_BW`-wrapped) driver that packs internally (e.g. GxEPD2_583/UC8159c's `_send8pixel` - a 1bpp buffer expanded to a 2-code wire format). That case is covered by `emit_descriptor.h`'s `make_resolver_bw_packed` instead - folded into the one-command `tools/capture.py` capture, same as 4-colour/7-colour - not a separate diag program at all. - `spy/` — the host stand-ins for the Arduino/SPI APIs a GxEPD2 driver calls; `trace.h` records what the driver does.