Эх сурвалжийг харах

Panel registry with verification status as data

display.py's MODELS becomes PANELS: a namedtuple registry carrying
descriptor/part/controller/format/status per model - the single source of
truth for what ships and whether it's hardware-verified. to_code() passes the
status to the component (set_verified), and dump_config() logs it at boot plus
a warning for non-verified panels so a user flashing a "structural" descriptor
knows it may be wrong and to report back. tools/panel_table.py renders the
supported-panels table from PANELS by AST (no esphome import); capture.py's
--list "shipped" marker updated for the new value form.
ZiB 3 долоо хоног өмнө
parent
commit
360f1de084

+ 31 - 25
components/gxepd2/display.py

@@ -6,6 +6,7 @@ Panels are selected via `model:`, each backed by an EpdPanelDescriptor
 """
 
 import re
+from collections import namedtuple
 
 import esphome.codegen as cg
 import esphome.config_validation as cv
@@ -43,28 +44,32 @@ CONF_INVERT_MONO = "invert_mono"
 gxepd2_ns = cg.esphome_ns.namespace("gxepd2")
 GxEPD2Display = gxepd2_ns.class_("GxEPD2Display", display.DisplayBuffer)
 
-# model name -> (C++ descriptor global declared in the matching panel_*.h
-# header - gxepd2_display.h includes them all, so no extra include wiring here;
-# Good Display part number on the panel's silkscreen).
+# The panel registry - the single source of truth for which panels ship, their
+# metadata, and whether each is hardware-verified. tools/panel_table.py renders
+# the supported-panels table from this, and set_verified() surfaces the status
+# in the device's boot log.
 #
-# The part number, not the GxEPD2 class name, is the source of truth for the
-# on-screen label (see _panel_label / test_pattern): GxEPD2's older naming
-# style drops the GDEx prefix and the 4-digit size - GxEPD2_213_Z98C is really
-# GDEY0213Z98 - so the class name alone can't reconstruct what's on the tag.
-MODELS = {
-    "GxEPD2_154_Z90c": ("PANEL_154_Z90C", "GDEH0154Z90"),
-    "GxEPD2_213_Z98C": ("PANEL_213_Z98C", "GDEY0213Z98"),
-    "GxEPD2_213c_GDEY0213F51": ("PANEL_213C_GDEY0213F51", "GDEY0213F51"),
-    "GxEPD2_213c_GDEY0213F52": ("PANEL_213C_GDEY0213F52", "GDEY0213F52"),
-    "GxEPD2_426_GDEQ0426T82": ("PANEL_426_GDEQ0426T82", "GDEQ0426T82"),
-    "GxEPD2_583_GDEQ0583T31": ("PANEL_583_GDEQ0583T31", "GDEQ0583T31"),
-    "GxEPD2_290c_GDEY029F51": ("PANEL_290C_GDEY029F51", "GDEY029F51"),
-    "GxEPD2_579c_GDEY0579F51": ("PANEL_579C_GDEY0579F51", "GDEY0579F51"),
-    # Debug variant: master half only, as if it were a self-contained
-    # 396x272 panel - see panel_579c_gdey0579f51_master_only.h. Kept
-    # around as a diagnostic tool, not the panel's real mode. Same physical
-    # part as the full panel, so same label.
-    "GxEPD2_579c_GDEY0579F51_MASTER_ONLY": ("PANEL_579C_GDEY0579F51_MASTER_ONLY", "GDEY0579F51"),
+# descriptor: the C++ EpdPanelDescriptor global declared in the matching
+#   panel_*.h (all `#include`d by gxepd2_display.h, so no extra wiring here).
+# part: the Good Display part number on the silkscreen - the source of truth for
+#   the on-screen label (GxEPD2's class name drops the GDEx prefix and size, so
+#   GxEPD2_213_Z98C alone can't reconstruct GDEY0213Z98).
+# status: "verified" (rendered correctly on real hardware), "structural"
+#   (descriptor captured and compiles, not yet bench-confirmed), or "diagnostic".
+Panel = namedtuple("Panel", "descriptor part controller fmt status")
+
+PANELS = {
+    "GxEPD2_154_Z90c": Panel("PANEL_154_Z90C", "GDEH0154Z90", "SSD1681", "3-color", "verified"),
+    "GxEPD2_213_Z98C": Panel("PANEL_213_Z98C", "GDEY0213Z98", "SSD1680", "3-color", "verified"),
+    "GxEPD2_213c_GDEY0213F51": Panel("PANEL_213C_GDEY0213F51", "GDEY0213F51", "JD79661", "4-color", "structural"),
+    "GxEPD2_213c_GDEY0213F52": Panel("PANEL_213C_GDEY0213F52", "GDEY0213F52", "JD79676", "4-color", "verified"),
+    "GxEPD2_426_GDEQ0426T82": Panel("PANEL_426_GDEQ0426T82", "GDEQ0426T82", "SSD1677", "mono", "verified"),
+    "GxEPD2_583_GDEQ0583T31": Panel("PANEL_583_GDEQ0583T31", "GDEQ0583T31", "UC8179", "mono", "verified"),
+    "GxEPD2_290c_GDEY029F51": Panel("PANEL_290C_GDEY029F51", "GDEY029F51", "JD79667", "4-color", "structural"),
+    "GxEPD2_579c_GDEY0579F51": Panel("PANEL_579C_GDEY0579F51", "GDEY0579F51", "HX8717", "4-color", "structural"),
+    # Diagnostic variant: the 396x272 master region as a self-contained panel.
+    "GxEPD2_579c_GDEY0579F51_MASTER_ONLY": Panel(
+        "PANEL_579C_GDEY0579F51_MASTER_ONLY", "GDEY0579F51", "HX8717", "4-color", "diagnostic"),
 }
 
 
@@ -81,7 +86,7 @@ CONFIG_SCHEMA = cv.All(
         {
             cv.GenerateID(): cv.declare_id(GxEPD2Display),
             cv.GenerateID(CONF_SPI_ID): cv.use_id(spi.SPIComponent),
-            cv.Required(CONF_MODEL): cv.one_of(*MODELS),
+            cv.Required(CONF_MODEL): cv.one_of(*PANELS),
             cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema,
             cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
             cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
@@ -103,8 +108,8 @@ CONFIG_SCHEMA = cv.All(
 async def to_code(config: ConfigType) -> None:
     var = cg.new_Pvariable(config[CONF_ID])
 
-    descriptor, part_number = MODELS[config[CONF_MODEL]]
-    cg.add(var.set_descriptor(cg.RawExpression(f"&esphome::gxepd2::{descriptor}")))
+    panel = PANELS[config[CONF_MODEL]]
+    cg.add(var.set_descriptor(cg.RawExpression(f"&esphome::gxepd2::{panel.descriptor}")))
     cg.add(var.set_cs_pin(await cg.gpio_pin_expression(config[CONF_CS_PIN])))
     cg.add(var.set_dc_pin(await cg.gpio_pin_expression(config[CONF_DC_PIN])))
     if reset_pin := config.get(CONF_RESET_PIN):
@@ -122,7 +127,8 @@ async def to_code(config: ConfigType) -> None:
     # Panel label for test_pattern(): the Good Display part number minus its
     # series prefix (GDEQ0583T31 -> 0583T31), so a tested panel shows what it
     # is on the shelf, not the GxEPD2 C++ class id.
-    cg.add(var.set_model_name(_panel_label(part_number)))
+    cg.add(var.set_model_name(_panel_label(panel.part)))
+    cg.add(var.set_verified(panel.status == "verified"))
 
     # A top-level `lambda:` must be processed and attached via set_writer()
     # here - register_display() only wires up `pages:` (same pattern as every

+ 9 - 0
components/gxepd2/gxepd2_display.h

@@ -34,6 +34,9 @@ class GxEPD2Display : public display::DisplayBuffer {
   // The short panel label (e.g. "GDEQ0583T31") test_pattern() can print,
   // derived from model: in display.py.
   void set_model_name(const char *name) { this->model_name_ = name; }
+  // Whether this panel's descriptor is confirmed on real hardware (vs captured
+  // but not yet bench-verified) - surfaced in dump_config(). From display.py.
+  void set_verified(bool v) { this->verified_ = v; }
 
   // Bench visual-check pattern, bit-depth aware. Everything scales to what
   // THIS panel can actually show (see test_colors_): the color-bar count,
@@ -212,6 +215,11 @@ class GxEPD2Display : public display::DisplayBuffer {
     ESP_LOGCONFIG(TAG, "  Busy active level: %s", this->desc_->busy_active_level ? "HIGH" : "LOW");
     if (this->desc_->bits_per_pixel == 1)
       ESP_LOGCONFIG(TAG, "  invert_mono: %s", YESNO(this->invert_mono_));
+    ESP_LOGCONFIG(TAG, "  Hardware-verified: %s", YESNO(this->verified_));
+    if (!this->verified_)
+      ESP_LOGW(TAG, "Panel %s is not yet hardware-verified - the captured descriptor may be wrong; "
+                    "please report what you see (see claude/DESIGN.md)",
+               this->model_name_ != nullptr ? this->model_name_ : "(unknown)");
     LOG_UPDATE_INTERVAL(this);
   }
 
@@ -348,6 +356,7 @@ class GxEPD2Display : public display::DisplayBuffer {
 
   const EpdPanelDescriptor *desc_{nullptr};
   const char *model_name_{nullptr};
+  bool verified_{false};
   GPIOPin *cs_{nullptr};
   GPIOPin *dc_{nullptr};
   GPIOPin *rst_{nullptr};

+ 1 - 1
tools/capture.py

@@ -48,7 +48,7 @@ def shipped_models() -> set:
         text = DISPLAY_PY.read_text()
     except OSError:
         return set()
-    return set(re.findall(r'"(GxEPD2_[^"]+)":\s*\(', text))
+    return set(re.findall(r'"(GxEPD2_[^"]+)":\s*Panel\(', text))
 
 
 def load_catalog() -> dict:

+ 48 - 0
tools/panel_table.py

@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+"""Render the supported-panels table from display.py's PANELS registry.
+
+PANELS (in components/gxepd2/display.py) is the single source of truth for
+which panels ship and their status. This reads it by AST (no esphome import)
+and prints a Markdown table for the README / DESIGN.md.
+
+    tools/panel_table.py
+"""
+
+import ast
+import pathlib
+
+DISPLAY_PY = pathlib.Path(__file__).resolve().parent.parent / "components" / "gxepd2" / "display.py"
+
+STATUS = {
+    "verified": "✅ verified",
+    "structural": "⚪ structural",
+    "diagnostic": "🔧 diagnostic",
+}
+
+
+def panels():
+    tree = ast.parse(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):
+                model = key.value
+                # val is a Call: Panel(descriptor, part, controller, fmt, status)
+                descriptor, part, controller, fmt, status = (a.value for a in val.args)
+                yield model, part, controller, fmt, status
+            return
+    raise SystemExit("PANELS not found in display.py")
+
+
+def main():
+    rows = list(panels())
+    print("| `model:` | Panel | Controller | Format | Status |")
+    print("|---|---|---|---|---|")
+    for model, part, controller, fmt, status in rows:
+        print(f"| `{model}` | {part} | {controller} | {fmt} | {STATUS.get(status, status)} |")
+    n_ver = sum(1 for *_, s in rows if s == "verified")
+    print(f"\n{len(rows)} panels; {n_ver} hardware-verified. "
+          "✅ rendered correctly on real hardware · ⚪ captured, not yet bench-confirmed · 🔧 diagnostic variant.")
+
+
+if __name__ == "__main__":
+    main()