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

Add a dynamic-tracing harness for extracting GxEPD2 per-panel protocol data

Compiles a real vendor driver against spy Arduino.h/SPI.h/Adafruit_GFX.h
stand-ins and records its actual command/data trace, instead of statically
parsing source - sidesteps needing an expression evaluator or call-graph
logic entirely, since it just executes the real logic and observes what
comes out.

Two variants: run_harness.h calls writeImage()/refresh() directly on the
bare driver; run_wrapper_harness.h drives the real GxEPD2_BW<Driver,H>
wrapper template (matching how an actual sketch, and the pre-rewrite
gxepd2 component, use it) via firstPage()/fillScreen()+drawPixel()/
nextPage(). Both traced clean for GxEPD2_213_Z98c, GxEPD2_154_Z90c, and
GxEPD2_290_T94, matching the hand-written panel_213_z98c.h byte-for-byte
and surfacing real behavior (an _initial_write priming pass, and a
partial-update-prep dual-write dance in the wrapper path) that reading
source by hand had missed.

Exploratory pass only - not yet wired into a JSON/descriptor output. See
the plan file and project memory for findings and next steps (a hardware
test list across ~6 physical panels, including a dual-CS and some
current/previous-shape mono panels, to validate the no-partial-refresh
capture cutoff this pass concluded on).
ZiB 1 сар өмнө
parent
commit
42c87bb88d

+ 8 - 0
tools/gxepd2_harness/run_154_z90c.cpp

@@ -0,0 +1,8 @@
+#include "run_harness.h"
+
+#include "epd3c/GxEPD2_154_Z90c.h"
+
+int main() {
+  run_harness<GxEPD2_154_Z90c>("GxEPD2_154_Z90c");
+  return 0;
+}

+ 8 - 0
tools/gxepd2_harness/run_213_z98c.cpp

@@ -0,0 +1,8 @@
+#include "run_harness.h"
+
+#include "epd3c/GxEPD2_213_Z98c.h"
+
+int main() {
+  run_harness<GxEPD2_213_Z98c>("GxEPD2_213_Z98c");
+  return 0;
+}

+ 8 - 0
tools/gxepd2_harness/run_290_t94.cpp

@@ -0,0 +1,8 @@
+#include "run_harness.h"
+
+#include "epd/GxEPD2_290_T94.h"
+
+int main() {
+  run_harness<GxEPD2_290_T94>("GxEPD2_290_T94");
+  return 0;
+}

+ 26 - 0
tools/gxepd2_harness/run_290_t94_wrapper.cpp

@@ -0,0 +1,26 @@
+// The driver header must come before GxEPD2_BW.h - it defines the
+// GxEPD_WHITE/BLACK/RED macros run_wrapper_harness.h's template body uses
+// (macros are purely textual, need to exist before that text is seen), and
+// GxEPD2_BW.h itself has a handful of bare `WIDTH`/`HEIGHT` references in
+// its method bodies that aren't qualified with `GxEPD2_Type::` (most uses
+// in that file are; a few aren't - looks like a latent vendor oversight
+// that real Arduino board builds happen not to hit, presumably because
+// some board variant header defines WIDTH/HEIGHT globals GxEPD2_BW.h's
+// unqualified uses silently pick up). Defining the same two names as
+// ordinary global constants (not macros, to avoid also rewriting the
+// already-qualified `GxEPD2_Type::WIDTH` occurrences) before including
+// GxEPD2_BW.h satisfies plain unqualified name lookup the same way.
+#include "epd/GxEPD2_290_T94.h"
+constexpr int16_t WIDTH = GxEPD2_290_T94::WIDTH;
+constexpr int16_t HEIGHT = GxEPD2_290_T94::HEIGHT;
+#include "GxEPD2_BW.h"
+
+#include "run_wrapper_harness.h"
+
+using Driver = Peek<GxEPD2_290_T94>;
+using Wrapper = GxEPD2_BW<Driver, GxEPD2_290_T94::HEIGHT>;
+
+int main() {
+  run_wrapper_harness<Wrapper, Driver>("GxEPD2_290_T94", /*three_color=*/false);
+  return 0;
+}

+ 61 - 0
tools/gxepd2_harness/run_harness.h

@@ -0,0 +1,61 @@
+#pragma once
+
+// Drives one vendor GxEPD2 driver through the same low-level calls the
+// GxEPD2_BW/GxEPD2_3C wrapper templates make for a single non-paged full
+// update (writeImage() then refresh()) - bypassing the wrapper templates
+// themselves (and their Adafruit_GFX dependency) since we only need the
+// driver-level trace, not drawing helpers.
+
+#include <cstdio>
+#include <cstring>
+
+#include "spy/Arduino.h"
+#include "spy/SPI.h"
+#include "trace.h"
+
+// Exposes GxEPD2_EPD's protected busy_level/reset_duration without needing
+// to parse anything - a subclass can read its own base class's protected
+// members directly.
+template<typename Driver> struct Peek : Driver {
+  using Driver::Driver;
+  int busy_level() const { return this->_busy_level; }
+  uint16_t reset_duration() const { return this->_reset_duration; }
+};
+
+// planes=1 for monochrome drivers (color buffer still passed - harmless,
+// see e.g. GxEPD2_290_T94::writeImage(black,color,...) which just ignores
+// a non-null color pointer's absence).
+template<typename Driver> void run_harness(const char *name) {
+  printf("=== %s: WIDTH=%u HEIGHT=%u ===\n", name, (unsigned) Driver::WIDTH, (unsigned) Driver::HEIGHT);
+
+  constexpr int16_t CS = 10, DC = 11, RST = 12, BUSY = 13;
+  Trace::set_dc_pin(DC);
+
+  Peek<Driver> driver(CS, DC, RST, BUSY);
+  printf("busy_level=%d reset_duration=%u\n", driver.busy_level(), driver.reset_duration());
+  // digitalRead(BUSY) should report "released" immediately - opposite of
+  // busy_level - so _waitWhileBusy()'s loop exits after one check.
+  Trace::last_busy_release_value = driver.busy_level() == HIGH ? LOW : HIGH;
+
+  driver.selectSPI(SPI, SPISettings());
+
+  printf("--- init(0) ---\n");
+  driver.init(0);
+
+  const uint32_t plane_bytes = (static_cast<uint32_t>(Driver::WIDTH) * Driver::HEIGHT) / 8;
+  auto *black = new uint8_t[plane_bytes];
+  auto *color = new uint8_t[plane_bytes];
+  // Distinctive, non-0x00/0xFF pattern - lets a plane's invert flag be
+  // read directly off the trace (does the wire byte equal 0xA5 or 0x5A).
+  memset(black, 0xA5, plane_bytes);
+  memset(color, 0xA5, plane_bytes);
+
+  printf("--- writeImage(black, color, 0, 0, WIDTH, HEIGHT) ---\n");
+  driver.writeImage(black, color, 0, 0, Driver::WIDTH, Driver::HEIGHT);
+
+  printf("--- refresh(false) ---\n");
+  driver.refresh(false);
+
+  delete[] black;
+  delete[] color;
+}

+ 54 - 0
tools/gxepd2_harness/run_wrapper_harness.h

@@ -0,0 +1,54 @@
+#pragma once
+
+// Drives a real GxEPD2_BW<Driver,H>/GxEPD2_3C<Driver,H> wrapper template -
+// the actual class GxEPD2 example sketches (and the pre-rewrite gxepd2
+// component) instantiate - through one full-screen update via its real
+// public API (init/setFullWindow/firstPage/fillScreen+drawPixel/nextPage),
+// instead of calling the bare driver's writeImage()/refresh() ourselves.
+// This is what actually exercises whatever the wrapper's own firstPage()/
+// nextPage() do internally (writeImageForFullRefresh, embedded priming,
+// etc.) - which run_harness.h's direct calls don't necessarily match.
+
+#include <cstdio>
+
+#include "spy/Adafruit_GFX.h"
+#include "spy/Arduino.h"
+#include "spy/SPI.h"
+#include "trace.h"
+
+template<typename Driver> struct Peek : Driver {
+  using Driver::Driver;
+  int busy_level() const { return this->_busy_level; }
+  uint16_t reset_duration() const { return this->_reset_duration; }
+};
+
+// WrapperT: GxEPD2_BW<PeekDriverT, H> or GxEPD2_3C<PeekDriverT, H> -
+// the caller picks which, since that's a fact about the panel (its
+// catalog.json "wrapper" field), not something this harness infers.
+// PeekDriverT: the same Peek<Driver> passed as WrapperT's first template
+// argument - repeated here since GxEPD2_BW/3C don't expose it as a nested
+// typedef we could pull back out.
+template<typename WrapperT, typename PeekDriverT> void run_wrapper_harness(const char *name, bool three_color) {
+  printf("=== %s (via wrapper template) ===\n", name);
+
+  constexpr int16_t CS = 10, DC = 11, RST = 12, BUSY = 13;
+  Trace::set_dc_pin(DC);
+
+  WrapperT display(PeekDriverT(CS, DC, RST, BUSY));
+  printf("busy_level=%d reset_duration=%u\n", display.epd2.busy_level(), display.epd2.reset_duration());
+  Trace::last_busy_release_value = display.epd2.busy_level() == HIGH ? LOW : HIGH;
+  display.epd2.selectSPI(SPI, SPISettings());
+
+  printf("--- init(0) ---\n");
+  display.init(0);
+
+  printf("--- setFullWindow/firstPage/fillScreen+drawPixel/nextPage ---\n");
+  display.setFullWindow();
+  display.firstPage();
+  do {
+    display.fillScreen(GxEPD_WHITE);
+    display.drawPixel(0, 0, GxEPD_BLACK);
+    if (three_color)
+      display.drawPixel(1, 0, GxEPD_RED);
+  } while (display.nextPage());
+}

+ 33 - 0
tools/gxepd2_harness/spy/Adafruit_GFX.h

@@ -0,0 +1,33 @@
+#pragma once
+
+// Minimal stand-in for the parts of Adafruit_GFX that GxEPD2_BW.h/
+// GxEPD2_3C.h's wrapper templates actually use (constructor(w,h), width()/
+// height(), rotation, fillScreen() looping drawPixel(), drawPixel() left
+// pure virtual for the wrapper to override). Not vendoring the real
+// Adafruit_GFX (fonts, shape-drawing, etc.) - none of that is exercised by
+// driving one full-screen update through firstPage()/nextPage().
+
+#include <cstdint>
+
+class Adafruit_GFX {
+ public:
+  Adafruit_GFX(int16_t w, int16_t h) : width_(w), height_(h) {}
+  virtual ~Adafruit_GFX() = default;
+
+  int16_t width() const { return rotation_ & 1 ? height_ : width_; }
+  int16_t height() const { return rotation_ & 1 ? width_ : height_; }
+  uint8_t getRotation() const { return rotation_; }
+  void setRotation(uint8_t r) { rotation_ = r & 3; }
+
+  virtual void drawPixel(int16_t x, int16_t y, uint16_t color) = 0;
+
+  void fillScreen(uint16_t color) {
+    for (int16_t y = 0; y < height(); y++)
+      for (int16_t x = 0; x < width(); x++)
+        drawPixel(x, y, color);
+  }
+
+ protected:
+  int16_t width_, height_;
+  uint8_t rotation_{0};
+};

+ 56 - 0
tools/gxepd2_harness/spy/Arduino.h

@@ -0,0 +1,56 @@
+#pragma once
+
+// Minimal standalone Arduino.h stand-in for compiling one vendor GxEPD2
+// driver on a dev machine (plain g++/clang, not the ESP-IDF toolchain) so
+// its real command/data byte trace can be observed directly, instead of
+// statically parsed. Shape mirrors ../../components/gxepd2/arduino_compat/
+// Arduino.h (same symbols GxEPD2 needs), but every call here logs to
+// ../trace.h instead of touching real hardware via esphome.
+
+#include <cstdint>
+#include <cstdio>
+#include <sstream>
+#include <string>
+
+#include "../trace.h"
+
+// Vendor source branches on this to pick <pgmspace.h> over <avr/pgmspace.h>
+// - defining it is simpler than providing a fake avr/ include directory.
+#ifndef ESP32
+#define ESP32
+#endif
+
+#define HIGH 1
+#define LOW 0
+#define INPUT 0
+#define OUTPUT 1
+#define INPUT_PULLUP 2
+
+inline void pinMode(int16_t pin, uint8_t mode) { Trace::pin_mode(pin, mode); }
+
+inline void digitalWrite(int16_t pin, uint8_t value) {
+  Trace::digital_write(pin, value);
+  Trace::note_digital_write_for_dc(pin, value);
+}
+
+inline int digitalRead(int16_t pin) { return Trace::digital_read(pin); }
+
+inline void delay(uint32_t ms) { Trace::delay_ms(ms); }
+inline uint32_t millis() { return 0; }
+inline uint32_t micros() { return 0; }
+inline void yield() {}
+
+#define pgm_read_byte(addr) (*reinterpret_cast<const uint8_t *>(addr))
+
+// GxEPD2_EPD::_waitWhileBusy()'s diagnostic prints (opt-in, never enabled
+// here since the harness passes serial_diag_bitrate=0 to init()) and the
+// unconditional "Busy Timeout!" warning both go through this - harmless to
+// drop for this pass, we already observe the busy-check via digitalRead().
+class HarnessSerialShim {
+ public:
+  void begin(uint32_t) {}
+  template<typename T> void print(T value) {}
+  template<typename T> void println(T value) {}
+  void println() {}
+};
+inline HarnessSerialShim Serial;

+ 33 - 0
tools/gxepd2_harness/spy/SPI.h

@@ -0,0 +1,33 @@
+#pragma once
+
+// Minimal standalone SPI.h stand-in - see Arduino.h in this same directory
+// for why this exists. Every byte transfer is logged with the DC pin's
+// current logical level (tracked by trace.h from digitalWrite() calls),
+// which is what lets the harness tell a command byte from a data byte
+// without any driver-specific knowledge.
+
+#include <cstdint>
+
+#include "../trace.h"
+
+class SPISettings {
+ public:
+  SPISettings() = default;
+  SPISettings(uint32_t clock, uint8_t bit_order, uint8_t data_mode) {
+    Trace::spi_settings(clock, bit_order, data_mode);
+  }
+};
+
+#define MSBFIRST 0
+#define SPI_MODE0 0
+
+class SPIClass {
+ public:
+  void begin() {}
+  void end() {}
+  void beginTransaction(SPISettings) {}
+  void endTransaction() {}
+  uint8_t transfer(uint8_t data) { return Trace::spi_transfer(data); }
+};
+
+inline SPIClass SPI;

+ 4 - 0
tools/gxepd2_harness/spy/pgmspace.h

@@ -0,0 +1,4 @@
+#pragma once
+// Empty stand-in - pgm_read_byte is a plain macro defined in Arduino.h
+// here (no real PROGMEM distinction on a dev machine), so nothing from
+// this header is actually needed.

+ 54 - 0
tools/gxepd2_harness/trace.h

@@ -0,0 +1,54 @@
+#pragma once
+
+// Flat, ungrouped event log the spy Arduino.h/SPI.h shims append to as a
+// real vendor GxEPD2 driver runs. Deliberately NOT grouped into
+// EpdCommand-shaped entries here - this exploratory pass prints the raw
+// observed event stream and compares it against the hand-written
+// panel_213_z98c.h by eye, to learn what the trace actually looks like
+// before building a grouping/JSON-emitting post-processor.
+
+#include <cstdint>
+#include <cstdio>
+
+enum class EventKind { PIN_MODE, DIGITAL_WRITE, DIGITAL_READ, DELAY, SPI_BEGIN_TXN, SPI_TRANSFER, SPI_SETTINGS };
+
+struct Trace {
+  static void pin_mode(int pin, int mode) { printf("pinMode(%d, %d)\n", pin, mode); }
+
+  static void digital_write(int pin, int value) { printf("digitalWrite(%d, %d)\n", pin, value); }
+
+  // Every digitalRead() in these drivers is a busy-pin poll - there's
+  // nothing else wired as an input. Log it as such and always report
+  // "released" so _waitWhileBusy()'s loop exits after one check; we
+  // don't need real timing, just to observe *that* a wait happened here.
+  static int digital_read(int pin) {
+    printf("digitalRead(%d) -> busy-check, reporting released\n", pin);
+    return last_busy_release_value;
+  }
+
+  static void delay_ms(unsigned long ms) { printf("delay(%lu)\n", ms); }
+
+  static void spi_settings(uint32_t clock_hz, uint8_t bit_order, uint8_t data_mode) {
+    printf("SPISettings(clock_hz=%u, bit_order=%u, data_mode=%u)\n", (unsigned) clock_hz, bit_order, data_mode);
+  }
+
+  // dc_state: current logical level of the DC pin at the moment of this
+  // transfer, as last set via digital_write() on the pin number the
+  // harness tells us is DC (see set_dc_pin()). true = command byte,
+  // false = data byte - matches GxEPD2_EPD::_writeCommand()/_writeData()
+  // exactly (DC low while the command byte goes out, high for data).
+  static uint8_t spi_transfer(uint8_t byte) {
+    printf("SPI.transfer(0x%02X) dc=%s\n", byte, dc_is_low ? "LOW(cmd)" : "HIGH(data)");
+    return 0;
+  }
+
+  static void set_dc_pin(int pin) { dc_pin = pin; }
+  static void note_digital_write_for_dc(int pin, int value) {
+    if (pin == dc_pin)
+      dc_is_low = (value == 0);
+  }
+
+  static inline int dc_pin = -1;
+  static inline bool dc_is_low = false;
+  static inline int last_busy_release_value = 0;  // overwritten by harness once busy_level is known
+};