|
@@ -0,0 +1,141 @@
|
|
|
|
|
+#!/usr/bin/env python3
|
|
|
|
|
+"""Regression check: re-emit each shipped panel from its GxEPD2 driver and diff
|
|
|
|
|
+the result against the committed components/gxepd2/panel_*.h.
|
|
|
|
|
+
|
|
|
|
|
+Compares only the structure the emitter authoritatively produces - the INIT /
|
|
|
|
|
+WINDOW / REFRESH command sequences, plane count, plane-write command(s),
|
|
|
|
|
+window_per_plane, and plane_invert - not the hand/diag-filled fields
|
|
|
|
|
+(bits_per_pixel/palette/blank_code/plane_width/row_interleave), which carry
|
|
|
|
|
+comments and names the emitter doesn't reproduce.
|
|
|
|
|
+
|
|
|
|
|
+Catches two things: an emitter regression (reproduction breaks), and GxEPD2
|
|
|
|
|
+upstream drift (a vendor bump changes a driver -> its panel now differs ->
|
|
|
|
|
+review it). Exit non-zero on any unexpected mismatch.
|
|
|
|
|
+
|
|
|
|
|
+ tools/check_descriptors.py # check all
|
|
|
|
|
+ tools/check_descriptors.py -v # show the diff on a mismatch
|
|
|
|
|
+
|
|
|
|
|
+Panels the emitter does not yet fully model (dual-region, see KNOWN_DIFF) are
|
|
|
|
|
+expected to differ and don't fail; if one of them starts MATCHING, that's an
|
|
|
|
|
+emitter improvement - remove it from KNOWN_DIFF. Panels with no GxEPD2 driver
|
|
|
|
|
+source (e.g. the synthetic MASTER_ONLY variant) are skipped.
|
|
|
|
|
+"""
|
|
|
|
|
+
|
|
|
|
|
+import argparse
|
|
|
|
|
+import ast
|
|
|
|
|
+import pathlib
|
|
|
|
|
+import re
|
|
|
|
|
+import sys
|
|
|
|
|
+
|
|
|
|
|
+ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
|
|
|
+sys.path.insert(0, str(ROOT / "tools"))
|
|
|
|
|
+import capture as cap # noqa: E402 (resolution + emit, reused)
|
|
|
|
|
+
|
|
|
|
|
+PANEL_DIR = ROOT / "components" / "gxepd2"
|
|
|
|
|
+
|
|
|
|
|
+# Dual-region panels the emitter's single/shared-region segmentation doesn't
|
|
|
|
|
+# model yet (its init/refresh region-select layer). Expected to differ.
|
|
|
|
|
+KNOWN_DIFF = {"GxEPD2_579c_GDEY0579F51"}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def registry():
|
|
|
|
|
+ """(model, part) for every entry in display.py's PANELS."""
|
|
|
|
|
+ tree = ast.parse((PANEL_DIR / "display.py").read_text())
|
|
|
|
|
+ for node in ast.walk(tree):
|
|
|
|
|
+ if isinstance(node, ast.Assign) and any(getattr(t, "id", None) == "PANELS" for t in node.targets):
|
|
|
|
|
+ for key, val in zip(node.value.keys, node.value.values):
|
|
|
|
|
+ yield key.value
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def parse_descriptor(text: str) -> dict:
|
|
|
|
|
+ """Normalize a panel_*.h into the fields the emitter owns. Comments and
|
|
|
|
|
+ array names are stripped, so only the command bytes / flags are compared."""
|
|
|
|
|
+ text = re.sub(r"//.*", "", text)
|
|
|
|
|
+ text = re.sub(r"/\*.*?\*/", "", text, flags=re.S)
|
|
|
|
|
+ arrs = {name: [int(x, 16) for x in re.findall(r"0x[0-9A-Fa-f]+", body)]
|
|
|
|
|
+ for name, body in re.findall(r"uint8_t\s+(\w+)\[\]\s*=\s*\{([^}]*)\}", text)}
|
|
|
|
|
+ seqs = {}
|
|
|
|
|
+ for seqname, body in re.findall(r"EpdCommand\s+(\w+)\[\]\s*=\s*\{(.*?)\};", text, flags=re.S):
|
|
|
|
|
+ cmds = []
|
|
|
|
|
+ for line in re.findall(r"\{([^}]*)\}", body):
|
|
|
|
|
+ parts = [p.strip() for p in line.split(",")]
|
|
|
|
|
+ op = int(parts[0], 16)
|
|
|
|
|
+ data = tuple(arrs.get(parts[1], [])) if parts[1] != "nullptr" else ()
|
|
|
|
|
+ cmds.append((op, data, "true" in line))
|
|
|
|
|
+ role = ("INIT" if "INIT" in seqname else "WINDOW" if "WINDOW" in seqname
|
|
|
|
|
+ else "REFRESH" if "REFRESH" in seqname else seqname)
|
|
|
|
|
+ seqs.setdefault(role, cmds) # first WINDOW_SEQ wins (window_sequence[0])
|
|
|
|
|
+
|
|
|
|
|
+ def field(name, default="?"):
|
|
|
|
|
+ m = re.search(rf"\.{name}\s*=\s*([^,\n]+)", text)
|
|
|
|
|
+ return m.group(1).strip() if m else default
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ "INIT": seqs.get("INIT"),
|
|
|
|
|
+ "WINDOW": seqs.get("WINDOW", []),
|
|
|
|
|
+ "REFRESH": seqs.get("REFRESH"),
|
|
|
|
|
+ "plane_count": field("plane_count"),
|
|
|
|
|
+ "plane_write_cmd": field("plane_write_cmd"),
|
|
|
|
|
+ "window_per_plane": field("window_per_plane", "false"),
|
|
|
|
|
+ "plane_invert": field("plane_invert"),
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def check(model: str, catalog: dict, verbose: bool) -> str:
|
|
|
|
|
+ """Returns 'MATCH', 'DIFF', 'XFAIL', 'XPASS', 'SKIP', or 'ERROR'."""
|
|
|
|
|
+ panel_h = PANEL_DIR / cap.filename_of(model)
|
|
|
|
|
+ if not panel_h.exists():
|
|
|
|
|
+ return "SKIP"
|
|
|
|
|
+ src_rel, _, _ = cap.resolve_source(model, catalog)
|
|
|
|
|
+ if src_rel is None:
|
|
|
|
|
+ return "SKIP" # no GxEPD2 driver to re-emit from (e.g. synthetic variant)
|
|
|
|
|
+ try:
|
|
|
|
|
+ generated = cap.capture(model, catalog, debug=False, cxx="g++", keep=False)
|
|
|
|
|
+ except SystemExit as e:
|
|
|
|
|
+ print(f" {e}", file=sys.stderr)
|
|
|
|
|
+ return "ERROR"
|
|
|
|
|
+ g, h = parse_descriptor(generated), parse_descriptor(panel_h.read_text())
|
|
|
|
|
+ same = g == h
|
|
|
|
|
+ expected_diff = model in KNOWN_DIFF
|
|
|
|
|
+ if same:
|
|
|
|
|
+ return "XPASS" if expected_diff else "MATCH"
|
|
|
|
|
+ if expected_diff:
|
|
|
|
|
+ return "XFAIL"
|
|
|
|
|
+ if verbose:
|
|
|
|
|
+ for k in g:
|
|
|
|
|
+ if g[k] != h[k]:
|
|
|
|
|
+ print(f" {k}:\n emitted : {g[k]}\n committed: {h[k]}", file=sys.stderr)
|
|
|
|
|
+ return "DIFF"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def main() -> None:
|
|
|
|
|
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
|
|
|
+ ap.add_argument("-v", "--verbose", action="store_true", help="show the fields that differ on a mismatch")
|
|
|
|
|
+ args = ap.parse_args()
|
|
|
|
|
+
|
|
|
|
|
+ catalog = cap.load_catalog()
|
|
|
|
|
+ results = {}
|
|
|
|
|
+ for model in registry():
|
|
|
|
|
+ # capture.py prints its own [capture] line to stderr; keep output tidy.
|
|
|
|
|
+ res = check(model, catalog, args.verbose)
|
|
|
|
|
+ results[model] = res
|
|
|
|
|
+ print(f" {res:<6} {model}")
|
|
|
|
|
+
|
|
|
|
|
+ bad = [m for m, r in results.items() if r in ("DIFF", "ERROR", "XPASS")]
|
|
|
|
|
+ n_match = sum(1 for r in results.values() if r == "MATCH")
|
|
|
|
|
+ print(f"\n{n_match} reproduced, {sum(1 for r in results.values() if r=='XFAIL')} known-diff, "
|
|
|
|
|
+ f"{sum(1 for r in results.values() if r=='SKIP')} skipped.")
|
|
|
|
|
+ if bad:
|
|
|
|
|
+ print("\nFAILURES:")
|
|
|
|
|
+ for m in bad:
|
|
|
|
|
+ why = {"DIFF": "command structure changed (emitter regression or GxEPD2 drift) - review",
|
|
|
|
|
+ "ERROR": "capture failed to build/run",
|
|
|
|
|
+ "XPASS": "now reproduces - remove from KNOWN_DIFF"}[results[m]]
|
|
|
|
|
+ print(f" {results[m]:<6} {m}: {why}")
|
|
|
|
|
+ sys.exit(1)
|
|
|
|
|
+ print("OK")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+if __name__ == "__main__":
|
|
|
|
|
+ main()
|