capture.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. #!/usr/bin/env python3
  2. """One-command panel-descriptor capture.
  3. Resolves a GxEPD2 driver class to its vendored source, builds and runs a host
  4. harness that *observes* the driver, and prints a candidate EpdPanelDescriptor
  5. (the body of a `components/gxepd2/panel_<model>.h`). No ESP toolchain needed -
  6. just a host C++ compiler. See tools/gxepd2_harness/README.md and CONTRIBUTING.md.
  7. tools/capture.py GxEPD2_154_Z90c # candidate -> stdout
  8. tools/capture.py GxEPD2_154_Z90c -o panel.h # candidate -> file
  9. tools/capture.py --list # capturable driver classes
  10. tools/capture.py GxEPD2_154_Z90c --debug # also dump the capture structure
  11. The output is a *candidate*: review it, fill any TODO fields the diag passes
  12. own (bits_per_pixel/palette for packed panels; plane_width/row_interleave for
  13. dual-region), save it as components/gxepd2/panel_<model>.h, add it to
  14. display.py's MODELS, compile, and bench-verify before marking it verified.
  15. """
  16. import argparse
  17. import difflib
  18. import json
  19. import pathlib
  20. import re
  21. import subprocess
  22. import sys
  23. import tempfile
  24. ROOT = pathlib.Path(__file__).resolve().parent.parent # code/
  25. CATALOG = ROOT / "tools" / "gxepd2_catalog" / "catalog.json"
  26. VSRC = ROOT / "third_party" / "GxEPD2" / "src"
  27. HARNESS = ROOT / "tools" / "gxepd2_harness"
  28. DISPLAY_PY = ROOT / "components" / "gxepd2" / "display.py"
  29. PREFIX = "GxEPD2_"
  30. def macro_of(model: str) -> str:
  31. return "PANEL_" + model[len(PREFIX):].upper()
  32. def filename_of(model: str) -> str:
  33. return "panel_" + model[len(PREFIX):].lower() + ".h"
  34. def shipped_models() -> set:
  35. """Model keys already wired into display.py's MODELS, to flag in --list."""
  36. try:
  37. text = DISPLAY_PY.read_text()
  38. except OSError:
  39. return set()
  40. return set(re.findall(r'"(GxEPD2_[^"]+)":\s*Panel\(', text))
  41. def load_catalog() -> dict:
  42. if not CATALOG.exists():
  43. sys.exit(f"catalog not found: {CATALOG}\n(run tools/gxepd2_catalog/generate_catalog.py)")
  44. return json.loads(CATALOG.read_text())
  45. def capturable(entry: dict) -> bool:
  46. # The emitter instantiates Driver(cs, dc, rst, busy) - only the standard
  47. # 4-arg constructor works without special-casing.
  48. return entry.get("ctor_args") == 4 and (VSRC / entry["source"]).exists()
  49. def do_list(catalog: dict) -> None:
  50. shipped = shipped_models()
  51. rows = []
  52. for model, entry in sorted(catalog.items()):
  53. if not capturable(entry):
  54. continue
  55. mark = "shipped" if model in shipped else "-"
  56. rows.append((model, mark, entry.get("comment", "")))
  57. w = max(len(m) for m, _, _ in rows)
  58. print(f"{len(rows)} capturable driver classes (ctor_args=4, source present):\n")
  59. for model, mark, comment in rows:
  60. print(f" {model:<{w}} {mark:<7} {comment}")
  61. def resolve_source(model: str, catalog: dict):
  62. """(source relpath, ctor_args|None, real_class_name) or (None, None, None).
  63. Catalog first (it has ctor_args), else glob the vendored tree - so a driver
  64. the fork added but GxEPD2's example headers don't list (e.g. the F52) is
  65. still capturable. Matching is case-insensitive and returns the driver's
  66. REAL class name (the `model:` key may differ in case, e.g. GxEPD2_213_Z98C
  67. vs the class GxEPD2_213_Z98c)."""
  68. low = model.lower()
  69. for k, e in catalog.items():
  70. if k.lower() == low:
  71. return e["source"], e.get("ctor_args"), e.get("driver", k)
  72. hits = [p for p in VSRC.glob("**/GxEPD2_*.cpp") if p.stem.lower() == low]
  73. if len(hits) == 1:
  74. return str(hits[0].relative_to(VSRC)), None, hits[0].stem
  75. if len(hits) > 1:
  76. sys.exit(f"{model}: multiple sources found: {[str(h) for h in hits]}")
  77. return None, None, None
  78. def capture(model: str, catalog: dict, debug: bool, cxx: str, keep: bool) -> str:
  79. src_rel, ctor_args, real_class = resolve_source(model, catalog)
  80. if src_rel is None:
  81. names = set(catalog) | {p.stem for p in VSRC.glob("**/GxEPD2_*.cpp")}
  82. hint = difflib.get_close_matches(model, names, n=3)
  83. msg = f"unknown model: {model} (no {model}.cpp under {VSRC})"
  84. if hint:
  85. msg += "\ndid you mean: " + ", ".join(hint)
  86. sys.exit(msg)
  87. if ctor_args not in (None, 4):
  88. sys.exit(f"{model}: non-standard constructor (ctor_args={ctor_args}); "
  89. "the emitter only handles the 4-arg GxEPD2_EPD(cs,dc,rst,busy) form.")
  90. src = VSRC / src_rel
  91. hdr_rel = src_rel[:-4] + ".h" # epd3c/GxEPD2_154_Z90c.h
  92. if not (VSRC / hdr_rel).exists():
  93. sys.exit(f"driver header not found: {VSRC / hdr_rel}")
  94. macro = macro_of(model)
  95. with tempfile.TemporaryDirectory() as td:
  96. tdp = pathlib.Path(td)
  97. main_cpp = tdp / "emit_main.cpp"
  98. main_cpp.write_text(
  99. f'#include "emit_descriptor.h"\n'
  100. f'#include "{hdr_rel}"\n'
  101. f'int main() {{ emit::emit_descriptor<{real_class}>("{real_class}", "{macro}"); return 0; }}\n'
  102. )
  103. binary = tdp / "emit_main"
  104. compile_cmd = [
  105. cxx, "-std=c++17",
  106. "-I", str(HARNESS / "spy"), "-I", str(VSRC), "-I", str(HARNESS),
  107. str(main_cpp), str(VSRC / "GxEPD2_EPD.cpp"), str(src),
  108. "-o", str(binary),
  109. ]
  110. print(f"[capture] {model} <- third_party/GxEPD2/src/{src_rel}", file=sys.stderr)
  111. cc = subprocess.run(compile_cmd, capture_output=True, text=True)
  112. if cc.returncode != 0:
  113. sys.exit(f"compile failed:\n{cc.stderr}")
  114. env = {"EMIT_DEBUG": "1"} if debug else None
  115. run = subprocess.run([str(binary)], capture_output=True, text=True, env=env)
  116. if keep:
  117. kept = HARNESS / f"run_{model[len(PREFIX):].lower()}_emit.cpp"
  118. kept.write_text(main_cpp.read_text())
  119. print(f"[capture] kept emit main: {kept}", file=sys.stderr)
  120. if debug and run.stderr:
  121. print(run.stderr, file=sys.stderr)
  122. if run.returncode != 0:
  123. sys.exit(f"capture run failed (exit {run.returncode}):\n{run.stdout}\n{run.stderr}")
  124. marker = "// GENERATED by emit_descriptor"
  125. idx = run.stdout.find(marker)
  126. if idx < 0:
  127. sys.exit(f"no descriptor in output:\n{run.stdout}")
  128. descriptor = run.stdout[idx:]
  129. print(f"[capture] ok -> save as components/gxepd2/{filename_of(model)}, "
  130. f"add \"{model}\" to display.py MODELS", file=sys.stderr)
  131. return descriptor
  132. def main() -> None:
  133. ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
  134. ap.add_argument("model", nargs="?", help="GxEPD2 driver class, e.g. GxEPD2_154_Z90c")
  135. ap.add_argument("-o", "--out", help="write the candidate descriptor to this file (default: stdout)")
  136. ap.add_argument("--list", action="store_true", help="list capturable driver classes and exit")
  137. ap.add_argument("--debug", action="store_true", help="dump the image-stage command/burst structure to stderr")
  138. ap.add_argument("--keep", action="store_true", help="keep the generated emit main under tools/gxepd2_harness/")
  139. ap.add_argument("--cxx", default="g++", help="C++ compiler (default: g++)")
  140. args = ap.parse_args()
  141. catalog = load_catalog()
  142. if args.list:
  143. do_list(catalog)
  144. return
  145. if not args.model:
  146. ap.error("a model is required (or use --list)")
  147. descriptor = capture(args.model, catalog, args.debug, args.cxx, args.keep)
  148. if args.out:
  149. pathlib.Path(args.out).write_text(descriptor)
  150. print(f"[capture] wrote {args.out}", file=sys.stderr)
  151. else:
  152. sys.stdout.write(descriptor)
  153. if __name__ == "__main__":
  154. main()