Просмотр исходного кода

emitter: auto-fill 7-colour fields via emit_palette_7c.h

Same approach as the 4-colour resolver: drive the vendor driver's own
GxEPD2_7C wrapper, read each named colour's wire code, and validate
against the engine's GXEPD2_PALETTE_7C before filling
bits_per_pixel/palette/blank_code. A non-standard 7-colour map (e.g.
GDEP073E01's rotated codes) stays an explicit TODO with the observed
table instead of being silently mislabelled.

Also fixes the generated comment mislabelling a 4bpp panel as
"16-colour packed" (2^bpp) instead of its actual probed colour count.
ZiB 3 недель назад
Родитель
Сommit
f6874087be

+ 19 - 6
tools/capture.py

@@ -11,12 +11,14 @@ just a host C++ compiler. See tools/gxepd2_harness/README.md and CONTRIBUTING.md
     tools/capture.py --list                      # capturable driver classes
     tools/capture.py --list                      # capturable driver classes
     tools/capture.py GxEPD2_154_Z90c --debug     # also dump the capture structure
     tools/capture.py GxEPD2_154_Z90c --debug     # also dump the capture structure
 
 
-The output is a *candidate*: review it, fill any TODO fields still left (a
-7-colour panel's bits_per_pixel/palette, or plane_width/line_order for
-dual-region), save it as components/gxepd2/panel_<model>.h, add it to
-display.py's MODELS, compile, and bench-verify before marking it verified.
-4-colour (epd4c/) panels have bits_per_pixel/palette/blank_code auto-filled by
-observing the GxEPD2_4C wrapper (see gxepd2_harness/emit_palette_4c.h).
+The output is a *candidate*: review it, fill any TODO fields still left
+(plane_width/line_order for dual-region), save it as
+components/gxepd2/panel_<model>.h, add it to display.py's MODELS, compile,
+and bench-verify before marking it verified.
+4-colour (epd4c/) and 7-colour (epd7c/) panels have
+bits_per_pixel/palette/blank_code auto-filled by observing the vendor
+GxEPD2_4C/GxEPD2_7C wrapper (see gxepd2_harness/emit_palette_4c.h /
+emit_palette_7c.h).
 """
 """
 
 
 import argparse
 import argparse
@@ -166,6 +168,9 @@ def capture(model: str, catalog: dict, debug: bool, cxx: str, keep: bool) -> str
     # leaving them as TODO. The wrapper references WIDTH/HEIGHT unqualified, so
     # leaving them as TODO. The wrapper references WIDTH/HEIGHT unqualified, so
     # those must be defined before GxEPD2_4C.h (pulled in by emit_palette_4c.h).
     # those must be defined before GxEPD2_4C.h (pulled in by emit_palette_4c.h).
     is_4c = src_rel.replace("\\", "/").startswith("epd4c/")
     is_4c = src_rel.replace("\\", "/").startswith("epd4c/")
+    # 7-colour (epd7c/) panels: same approach as 4-colour, via the GxEPD2_7C
+    # wrapper (see gxepd2_harness/emit_palette_7c.h).
+    is_7c = src_rel.replace("\\", "/").startswith("epd7c/")
     if is_4c:
     if is_4c:
         main_src = (
         main_src = (
             f'#include "{hdr_rel}"\n'
             f'#include "{hdr_rel}"\n'
@@ -174,6 +179,14 @@ def capture(model: str, catalog: dict, debug: bool, cxx: str, keep: bool) -> str
             f'#include "emit_palette_4c.h"\n'
             f'#include "emit_palette_4c.h"\n'
             f'int main() {{ emit::emit_descriptor_4c<{real_class}>("{real_class}", "{macro}"); return 0; }}\n'
             f'int main() {{ emit::emit_descriptor_4c<{real_class}>("{real_class}", "{macro}"); return 0; }}\n'
         )
         )
+    elif is_7c:
+        main_src = (
+            f'#include "{hdr_rel}"\n'
+            f'constexpr int16_t WIDTH = {real_class}::WIDTH;\n'
+            f'constexpr int16_t HEIGHT = {real_class}::HEIGHT;\n'
+            f'#include "emit_palette_7c.h"\n'
+            f'int main() {{ emit::emit_descriptor_7c<{real_class}>("{real_class}", "{macro}"); return 0; }}\n'
+        )
     else:
     else:
         main_src = (
         main_src = (
             f'#include "emit_descriptor.h"\n'
             f'#include "emit_descriptor.h"\n'

+ 7 - 3
tools/gxepd2_harness/README.md

@@ -39,14 +39,18 @@ resolution / busy-polarity / timeout from the driver, and derives
 `GxEPD2_4C` wrapper, reads the wire code for each named colour, validates it
 `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` /
 against the engine's `GXEPD2_PALETTE_4C`, and fills `bits_per_pixel` /
 `palette` / `blank_code` automatically (an unexpected code map stays a TODO
 `palette` / `blank_code` automatically (an unexpected code map stays a TODO
-carrying the observed table). A **dual-region** panel (per-region select +
+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. A **dual-region** panel (per-region select +
 window, e.g. GDEY0579F51 / HX8717) is segmented too: its per-region init /
 window, e.g. GDEY0579F51 / HX8717) is segmented too: its per-region init /
 window / refresh, `plane_width` (decoded from the region window-set) and a
 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
 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
 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
 diag native-path pass to confirm. Review the result, add any packed-format
-fields still left as TODO — `bits_per_pixel` / `palette` for a 7-colour panel
-(no resolver yet) — then save it
+fields still left as TODO, then save it
 as `components/gxepd2/panel_<panel>.h`. `capture.py` also prints the exact
 as `components/gxepd2/panel_<panel>.h`. `capture.py` also prints the exact
 `PANELS` line to paste into `display.py` (key / descriptor macro / format
 `PANELS` line to paste into `display.py` (key / descriptor macro / format
 filled from the capture; `part` / `controller` scraped from the driver header
 filled from the capture; `part` / `controller` scraped from the driver header

+ 5 - 3
tools/gxepd2_harness/emit_descriptor.h

@@ -163,6 +163,7 @@ struct PackedResolved {
   const char *palette_expr = "nullptr";      // e.g. "GXEPD2_PALETTE_4C"
   const char *palette_expr = "nullptr";      // e.g. "GXEPD2_PALETTE_4C"
   const char *palette_len_expr = "0";        // e.g. "GXEPD2_PALETTE_4C_LEN"
   const char *palette_len_expr = "0";        // e.g. "GXEPD2_PALETTE_4C_LEN"
   int blank_code = 0;               // per-pixel clear code (observed white)
   int blank_code = 0;               // per-pixel clear code (observed white)
+  int color_count = -1;             // actual distinct colours probed; -1 => assume 1<<bits_per_pixel
   std::string note;                 // appended as a comment (observed table, or why unresolved)
   std::string note;                 // appended as a comment (observed table, or why unresolved)
 };
 };
 using PackedResolverFn = std::function<PackedResolved(uint8_t clear_fill, const PlaneInvert &pv)>;
 using PackedResolverFn = std::function<PackedResolved(uint8_t clear_fill, const PlaneInvert &pv)>;
@@ -477,7 +478,7 @@ void emit_descriptor_impl(const char *class_name, const char *macro, const Packe
   printf("// GENERATED by emit_descriptor from %s - candidate.\n", class_name);
   printf("// GENERATED by emit_descriptor from %s - candidate.\n", class_name);
   printf("// Command sequences, plane layout and plane_invert are observed here;\n");
   printf("// Command sequences, plane layout and plane_invert are observed here;\n");
   printf("// for a packed panel bits_per_pixel/palette/blank_code are probe-filled\n");
   printf("// for a packed panel bits_per_pixel/palette/blank_code are probe-filled\n");
-  printf("// when the colour family is recognised (4-colour), else left TODO. A\n");
+  printf("// when the colour family is recognised (4-colour/7-colour), else left TODO. A\n");
   printf("// dual-region panel also gets plane_width and a candidate line_order; the\n");
   printf("// dual-region panel also gets plane_width and a candidate line_order; the\n");
   printf("// exact PAIRED_EDGES row order still wants diag/driver confirmation.\n");
   printf("// exact PAIRED_EDGES row order still wants diag/driver confirmation.\n");
   printf("#pragma once\n#include \"epd_command.h\"\n\nnamespace esphome::gxepd2 {\n\n");
   printf("#pragma once\n#include \"epd_command.h\"\n\nnamespace esphome::gxepd2 {\n\n");
@@ -575,10 +576,11 @@ void emit_descriptor_impl(const char *class_name, const char *macro, const Packe
     // emit_palette_4c.h), it derived depth/palette/blank_code by observing the
     // emit_palette_4c.h), it derived depth/palette/blank_code by observing the
     // driver's own colour fills - emit those (computed once, above). Otherwise
     // driver's own colour fills - emit those (computed once, above). Otherwise
     // fall back to a TODO the diag pass fills in by hand (an unknown packed
     // fall back to a TODO the diag pass fills in by hand (an unknown packed
-    // family, e.g. 7-colour before its resolver exists).
+    // family, e.g. a packed panel outside the 4-colour/7-colour resolvers).
     if (pr.resolved) {
     if (pr.resolved) {
+      const int shown_colors = pr.color_count >= 0 ? pr.color_count : (1 << pr.bits_per_pixel);
       printf("    .bits_per_pixel = %d,  // probe: %d-colour packed%s%s\n", pr.bits_per_pixel,
       printf("    .bits_per_pixel = %d,  // probe: %d-colour packed%s%s\n", pr.bits_per_pixel,
-             1 << pr.bits_per_pixel, pr.note.empty() ? "" : " - ", pr.note.c_str());
+             shown_colors, pr.note.empty() ? "" : " - ", pr.note.c_str());
       printf("    .palette = %s,\n", pr.palette_expr);
       printf("    .palette = %s,\n", pr.palette_expr);
       printf("    .palette_len = %s,\n", pr.palette_len_expr);
       printf("    .palette_len = %s,\n", pr.palette_len_expr);
       printf("    .blank_code = 0x%02X,  // observed white fill code\n", pr.blank_code & 0xFF);
       printf("    .blank_code = 0x%02X,  // observed white fill code\n", pr.blank_code & 0xFF);

+ 150 - 0
tools/gxepd2_harness/emit_palette_7c.h

@@ -0,0 +1,150 @@
+#pragma once
+
+// 7-colour (ACeP, 4bpp packed) palette/blank-code resolver for the descriptor
+// emitter. Same approach as emit_palette_4c.h: drive the vendor driver's own
+// GxEPD2_7C wrapper (fillScreen of each named colour), read the wire code it
+// emits for each, then CONFIRM that table matches the engine's
+// GXEPD2_PALETTE_7C codes (epd_command.h) before referencing it. A panel
+// whose codes don't match stays unresolved and emits the old TODO plus the
+// observed table, so a surprise is visible, never silently mislabelled.
+//
+// Unlike 4-colour, GxEPD2_7C's color7() maps all seven GxEPD_* constants to
+// distinct codes (no collisions), so EXPECT_7C below is a 1:1 check against
+// GXEPD2_PALETTE_7C rather than a many-to-one map.
+//
+// Include order (tools/capture.py's generated 7C main handles this): the
+// driver header and GxEPD2_7C.h first (they define GxEPD_* and the
+// WIDTH/HEIGHT the wrapper references unqualified), then emit_descriptor.h,
+// then this file.
+
+#include <cstdint>
+#include <cstdio>
+#include <string>
+#include <vector>
+
+#include "GxEPD2_7C.h"
+#include "emit_descriptor.h"
+#include "trace.h"
+
+namespace emit {
+
+// The engine's canonical 7-colour map (mirrors epd_command.h GXEPD2_PALETTE_7C
+// / GxEPD2_7C.h color7()): the wire code each named colour must resolve to
+// for the standard palette to be the right answer.
+struct Expect7C {
+  uint16_t color;
+  uint8_t code;
+  const char *label;
+};
+inline const Expect7C EXPECT_7C[] = {
+    {GxEPD_BLACK, 0x00, "black"},  {GxEPD_WHITE, 0x01, "white"},
+    {GxEPD_GREEN, 0x02, "green"},  {GxEPD_BLUE, 0x03, "blue"},
+    {GxEPD_RED, 0x04, "red"},      {GxEPD_YELLOW, 0x05, "yellow"},
+    {GxEPD_ORANGE, 0x06, "orange"},
+};
+
+// First run of exactly plane_bytes data bytes in the current Trace::record
+// (one fillScreen flush), or empty. Mirrors emit_palette_4c.h / run_diagB_
+// harness.h.
+inline std::vector<uint8_t> first_plane_burst_7c(uint32_t plane_bytes) {
+  std::vector<uint8_t> run, best;
+  auto flush = [&]() {
+    if (run.size() == plane_bytes && best.empty())
+      best = run;
+    run.clear();
+  };
+  for (const auto &ev : Trace::record) {
+    if (ev.first)  // command byte: ends the current data run
+      flush();
+    else
+      run.push_back(ev.second);
+  }
+  flush();
+  return best;
+}
+
+// Build the 7-colour packed resolver for `Driver`. It instantiates the vendor
+// GxEPD2_7C<Peek<Driver>, HEIGHT> wrapper (single full-height page) and reads
+// each colour's code off the wire.
+template<typename Driver> PackedResolverFn make_resolver_7c() {
+  return [](uint8_t /*clear_fill*/, const PlaneInvert & /*pv*/) -> PackedResolved {
+    constexpr int bpp = 4, ppb = 2;  // 7-colour: 4 bits/pixel, 2 pixels/byte, MSB-first (high nibble first)
+    const uint16_t W = Driver::WIDTH, H = Driver::HEIGHT;
+    const uint32_t plane_bytes = (uint32_t) W * H * bpp / 8;
+
+    using Wrapper = GxEPD2_7C<Peek<Driver>, Driver::HEIGHT>;  // page_height=HEIGHT => one page
+    Wrapper *dp = new Wrapper(Peek<Driver>(10, 11, 12, 13));
+    Wrapper &display = *dp;
+    Trace::set_dc_pin(11);
+    Trace::last_busy_release_value = display.epd2.busy_level() == HIGH ? LOW : HIGH;
+    display.epd2.selectSPI(SPI, SPISettings());
+    display.init(0);
+    display.setFullWindow();
+
+    // Prime (untracked) so the first-page buffer prime isn't read as a fill.
+    display.firstPage();
+    do {
+      display.fillScreen(GxEPD_WHITE);
+    } while (display.nextPage());
+
+    auto code_of = [&](uint16_t color, bool &uniform) -> int {
+      Trace::start_record();
+      display.firstPage();
+      do {
+        display.fillScreen(color);
+      } while (display.nextPage());
+      Trace::stop_record();
+      auto b = first_plane_burst_7c(plane_bytes);
+      if (b.empty())
+        return -1;
+      uint8_t byte0 = b[0];
+      uint8_t f0 = (byte0 >> ((ppb - 1) * bpp)) & 0xF;  // MSB field (high nibble)
+      uniform = true;
+      for (int j = 1; j < ppb; j++)
+        if (((byte0 >> ((ppb - 1 - j) * bpp)) & 0xF) != f0)
+          uniform = false;
+      return f0;
+    };
+
+    const int ncol = (int) (sizeof(EXPECT_7C) / sizeof(EXPECT_7C[0]));
+    bool matches = true;
+    int white_code = -1;
+    std::string observed;
+    for (int k = 0; k < ncol; k++) {
+      bool uniform = false;
+      int code = code_of(EXPECT_7C[k].color, uniform);
+      char buf[48];
+      snprintf(buf, sizeof(buf), "%s%s=%d", k ? " " : "", EXPECT_7C[k].label, code);
+      observed += buf;
+      if (EXPECT_7C[k].color == GxEPD_WHITE)
+        white_code = code;
+      if (!uniform || code != (int) EXPECT_7C[k].code)
+        matches = false;
+    }
+    delete dp;
+
+    PackedResolved r;
+    if (matches) {
+      r.resolved = true;
+      r.bits_per_pixel = bpp;
+      r.color_count = ncol;
+      r.palette_expr = "GXEPD2_PALETTE_7C";
+      r.palette_len_expr = "GXEPD2_PALETTE_7C_LEN";
+      r.blank_code = white_code;  // panel's white/clear code (0x01 for GxEPD2 7C)
+    } else {
+      r.resolved = false;
+      r.note = "7C probe mismatch, observed [" + observed +
+               "] (expected black=0 white=1 green=2 blue=3 red=4 yellow=5 orange=6)";
+    }
+    return r;
+  };
+}
+
+// 7-colour capture entry: like emit_descriptor(), but resolves the packed
+// fields by observing the GxEPD2_7C wrapper. tools/capture.py calls this for
+// an epd7c/ driver.
+template<typename Driver> void emit_descriptor_7c(const char *class_name, const char *macro) {
+  emit_descriptor_impl<Driver>(class_name, macro, make_resolver_7c<Driver>());
+}
+
+}  // namespace emit