| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005 |
- #pragma once
- // Prototype: emit a candidate panel_*.h EpdPanelDescriptor by *observing* a
- // real vendor GxEPD2 driver run, instead of hand-transcribing its C++. This
- // closes the hand-derivation gap for the straight-line parts of a descriptor
- // (init / window / refresh command sequences + the plane-write command) by
- // phase-segmenting the captured event stream at the public-API boundary - no
- // edits to the vendor source.
- //
- // Staging (the trick that separates the phases without instrumenting the
- // vendor's private methods):
- // writeScreenBuffer() -> lazy _InitDisplay() + a plane-write burst (clear).
- // Everything before that burst is the INIT sequence;
- // the command right before it is the PLANE-WRITE cmd.
- // writeImage(pattern) -> init already done, so this is just the WINDOW set
- // (commands before the pattern burst) + plane write.
- // refresh() -> re-sets the window then triggers; dropping the
- // duplicate window command leaves the REFRESH seq.
- //
- // Out of scope here (comes from the diag Pattern A/B passes, not one capture):
- // bits_per_pixel / palette / plane_invert / line_order. Those are emitted
- // as TODO placeholders so the diff against the hand file is about segmentation.
- #include <cstdint>
- #include <cstdio>
- #include <cstdlib>
- #include <functional>
- #include <set>
- #include <string>
- #include <type_traits>
- #include <utility>
- #include <vector>
- #include "spy/Arduino.h"
- #include "spy/SPI.h"
- #include "trace.h"
- namespace emit {
- // A data burst at least this long is a pixel-plane write, not config data.
- constexpr size_t PLANE_BURST_MIN = 256;
- struct Cmd {
- uint8_t cmd;
- std::vector<uint8_t> data;
- bool wait_busy = false;
- unsigned long delay_after = 0;
- };
- // Expose GxEPD2_EPD's protected busy level without parsing (same trick as
- // run_harness.h's Peek).
- template<typename Driver> struct Peek : Driver {
- using Driver::Driver;
- int busy_level() const { return this->_busy_level; }
- };
- // Group a slice of the event stream into EpdCommand-shaped records. Events
- // before the first SPI *command* (the reset pulse + its delays) are skipped -
- // the engine owns the hardware reset, so it never belongs in a descriptor.
- inline std::vector<Cmd> group(size_t lo, size_t hi) {
- std::vector<Cmd> cmds;
- bool started = false;
- for (size_t i = lo; i < hi; i++) {
- const auto &e = Trace::events[i];
- switch (e.kind) {
- case EventKind::SPI_TRANSFER:
- if (e.is_command) {
- cmds.push_back(Cmd{e.byte});
- started = true;
- } else if (started) {
- cmds.back().data.push_back(e.byte);
- }
- break;
- case EventKind::DELAY:
- if (started)
- cmds.back().delay_after += e.ms;
- break;
- case EventKind::DIGITAL_READ: // the only reads are busy-pin polls
- if (started) {
- cmds.back().wait_busy = true;
- // The delay(1) GxEPD2_EPD::_waitWhileBusy() runs inside its poll
- // loop lands right before this read and is part of the busy-wait,
- // not an inter-command delay - our engine owns busy-waiting, so
- // fold it in rather than emit it. (This also absorbs a vendor's own
- // tiny settle delay right before a refresh trigger, e.g. 290c's
- // _refresh delay(1); immaterial, the panel asserts busy regardless.)
- cmds.back().delay_after = 0;
- }
- break;
- default:
- break; // DIGITAL_WRITE = reset/CS/DC toggles: engine's job, not data
- }
- }
- return cmds;
- }
- // Index of the first command whose data burst marks a plane write, or np
- // (cmds.size()) if there's none in this slice.
- inline size_t plane_index(const std::vector<Cmd> &cmds) {
- for (size_t i = 0; i < cmds.size(); i++)
- if (cmds[i].data.size() >= PLANE_BURST_MIN)
- return i;
- return cmds.size();
- }
- inline void print_data_array(const char *macro, const char *suffix, const std::vector<uint8_t> &d) {
- printf("inline const uint8_t %s_%s[] = {", macro, suffix);
- for (size_t i = 0; i < d.size(); i++)
- printf("%s0x%02X", i ? ", " : "", d[i]);
- printf("};\n");
- }
- // Print one EpdCommand{} initializer line, with the optional fields only when
- // they're non-default (matches the hand files' style).
- inline void print_cmd(const char *macro, const char *suffix, const Cmd &c) {
- if (c.data.empty() && !c.wait_busy && c.delay_after == 0) {
- printf(" {0x%02X, nullptr, 0},\n", c.cmd);
- return;
- }
- const char *dataref = c.data.empty() ? "nullptr" : nullptr;
- if (dataref)
- printf(" {0x%02X, nullptr, 0", c.cmd);
- else
- printf(" {0x%02X, %s_%s, %u", c.cmd, macro, suffix, (unsigned) c.data.size());
- if (c.wait_busy || c.delay_after)
- printf(", /*delay_before_ms=*/0, /*delay_after_ms=*/%lu, /*wait_busy_after=*/%s", c.delay_after,
- c.wait_busy ? "true" : "false");
- printf("},\n");
- }
- // First data byte of the burst written right after command `op`, or -1.
- inline int burst_byte_after(const std::vector<Cmd> &cmds, uint8_t op) {
- for (const auto &c : cmds)
- if (c.cmd == op && c.data.size() >= PLANE_BURST_MIN)
- return c.data[0];
- return -1;
- }
- // The full data burst written right after command `op`, or empty.
- inline std::vector<uint8_t> burst_data_after(const std::vector<Cmd> &cmds, uint8_t op) {
- for (const auto &c : cmds)
- if (c.cmd == op && c.data.size() >= PLANE_BURST_MIN)
- return c.data;
- return {};
- }
- // Detects the two-arg writeScreenBuffer(black_value, color_value) that colour
- // (plane_count > 1) drivers have and mono drivers don't.
- template<typename T, typename = void> struct has_wsb2 : std::false_type {};
- template<typename T>
- struct has_wsb2<T, std::void_t<decltype(std::declval<T &>().writeScreenBuffer(uint8_t{}, uint8_t{}))>>
- : std::true_type {};
- struct PlaneInvert {
- bool inv0 = false, inv1 = false;
- bool plane1_valid = false;
- bool is_1bpp = false; // white fill => an all-bits-same wire byte (not packed)
- int w0_white = -1, w1_red = -1;
- };
- // A packed panel's depth / palette / blank code, resolved by observation for
- // the panel families the emitter knows how to probe (currently 4-colour, via
- // emit_palette_4c.h). `emit_descriptor_impl` calls a resolver of this shape
- // only for a packed panel (the plane-invert probe said white fill isn't an
- // all-bits wire byte); a null or unresolved result keeps the old TODO fields,
- // so an unknown packed family still emits a reviewable placeholder rather than
- // a wrong palette.
- struct PackedResolved {
- bool resolved = false; // false => emit TODO placeholders
- int bits_per_pixel = 2; // 2 (4-colour) or 4 (7-colour)
- const char *palette_expr = "nullptr"; // e.g. "GXEPD2_PALETTE_4C"
- const char *palette_len_expr = "0"; // e.g. "GXEPD2_PALETTE_4C_LEN"
- 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)
- // Set by make_resolver_bw_packed (below): a still-GxEPD2_BW-wrapped (binary)
- // driver that nonetheless packs >1 bit/pixel on the wire internally, with no
- // colour wrapper to drive a la emit_palette_4c.h/7c.h - e.g. GxEPD2_583's
- // _send8pixel. Only two codes are ever observed (black_code, blank_code=
- // white code), so the emitter prints a local 2-entry palette array instead
- // of referencing a shared GXEPD2_PALETTE_* constant.
- bool two_code_bw = false;
- int black_code = -1;
- };
- using PackedResolverFn = std::function<PackedResolved(uint8_t clear_fill, const PlaneInvert &pv)>;
- // A generous, class-aware default for busy_timeout_ms. The timeout is only a
- // diagnostic ceiling - the engine hard-aborts a busy-wait that runs past it and
- // logs a failure, where GxEPD2 itself just keeps polling - so erring long costs
- // nothing on a healthy panel and the exact number is meant to be dev-tuned. The
- // vendor full_refresh_time is a soft estimate, so we take 2x it, then clamp up
- // to a per-depth floor so a panel that under-reports (or whose colour pass is
- // genuinely slow) still gets room: mono/3-colour fail fast, 4-colour and
- // 7-colour (ACeP) get progressively more headroom.
- inline uint32_t default_busy_timeout_ms(unsigned full_refresh_time, int bits_per_pixel) {
- uint32_t headroom = static_cast<uint32_t>(full_refresh_time) * 2;
- uint32_t floor = bits_per_pixel >= 4 ? 60000u : bits_per_pixel == 2 ? 45000u : 15000u;
- return headroom > floor ? headroom : floor;
- }
- // plane_invert by capture (see probe_plane_invert.h for the rationale): drive
- // known full-screen fills and read each plane's wire byte, reconciling against
- // the engine's fixed buffer convention (plane0 bit=white, plane1 bit=red). A
- // white-fill plane0 wire that isn't 0x00/0xFF means a packed (>1bpp) panel,
- // where plane_invert is unused and bits_per_pixel/palette come from the diag
- // pass instead.
- template<typename Driver> PlaneInvert probe_plane_invert_values(uint8_t p0cmd, uint8_t p1cmd, size_t plane_count) {
- constexpr int16_t CS = 10, DC = 11, RST = 12, BUSY = 13;
- Trace::set_dc_pin(DC);
- Peek<Driver> d(CS, DC, RST, BUSY);
- Trace::last_busy_release_value = d.busy_level() == HIGH ? LOW : HIGH;
- d.selectSPI(SPI, SPISettings());
- d.init(0);
- // plane_bytes sized for the 1bpp hypothesis - same assumption
- // emit_descriptor_impl's own main capture makes (see its `black`/`color`
- // buffers). A genuinely packed driver still re-packs this buffer through
- // its real plane-write path (e.g. GxEPD2_583's _send8pixel), so the wire
- // byte it reports is the true packed code, not a false 1bpp reading.
- const uint32_t plane_bytes = (static_cast<uint32_t>(Driver::WIDTH) * Driver::HEIGHT) / 8;
- auto *fillbuf = new uint8_t[plane_bytes];
- PlaneInvert r;
- // Drive the SAME call path (writeImage) that produced p0cmd/p1cmd, not
- // writeScreenBuffer: some drivers (e.g. GxEPD2_270, "current" buffer 0x13
- // vs image-plane 0x15) use a different command for the screen-buffer clear
- // than for writeImage's own plane write. Probing via writeScreenBuffer then
- // searches the trace for a command (p0cmd) that call never emits, silently
- // returning "not found" (-1) and misreporting a true 1bpp panel as packed.
- auto cap0 = [&](uint8_t value) -> int {
- memset(fillbuf, value, plane_bytes);
- Trace::start_event_record();
- d.writeImage(fillbuf, fillbuf, 0, 0, Driver::WIDTH, Driver::HEIGHT);
- Trace::stop_event_record();
- return burst_byte_after(group(0, Trace::events.size()), p0cmd);
- };
- r.w0_white = cap0(0xFF);
- delete[] fillbuf;
- r.is_1bpp = (r.w0_white == 0x00 || r.w0_white == 0xFF);
- r.inv0 = (r.w0_white == 0x00); // engine plane0 bit=white; white wire 0x00 => invert
- if (plane_count > 1) {
- if constexpr (has_wsb2<Driver>::value) {
- Trace::start_event_record();
- d.writeScreenBuffer((uint8_t) 0xFF, (uint8_t) 0x00); // all-red fill
- Trace::stop_event_record();
- r.w1_red = burst_byte_after(group(0, Trace::events.size()), p1cmd);
- r.plane1_valid = (r.w1_red == 0x00 || r.w1_red == 0xFF);
- r.inv1 = (r.w1_red == 0x00); // engine plane1 bit=red; red wire 0x00 => invert
- }
- }
- return r;
- }
- // Generic packed resolver for a driver with NO colour wrapper to drive (unlike
- // emit_palette_4c.h/7c.h, which observe a GxEPD2_4C/7C wrapper's named-colour
- // fills) - the gap GxEPD2_583 (UC8159c) exposed: it's wrapped GxEPD2_BW
- // (binary), but `_send8pixel` expands each 1bpp buffer byte into a wider wire
- // format using only two of its codes. Drives the plain Driver's own
- // writeImage(black,color,...) - the SAME call the main capture already makes
- // - with a uniform all-white (0xFF) and all-black (0x00) 1bpp buffer. If both
- // wire bursts are internally uniform (one repeated byte - true for a per-bit
- // code lookup with no cross-pixel dependency) and the byte count divides the
- // pixel count evenly, that ratio directly gives bits_per_pixel; the MSB field
- // of each uniform byte gives that colour's code. A driver whose packing
- // depends on more than one bit at a time (dithering, error diffusion) fails
- // the uniformity check and stays an honest TODO rather than a wrong guess.
- template<typename Driver> PackedResolverFn make_resolver_bw_packed(uint8_t p0cmd) {
- return [p0cmd](uint8_t /*clear_fill*/, const PlaneInvert & /*pv*/) -> PackedResolved {
- PackedResolved r;
- auto fail = [&](const char *why) {
- r.resolved = false;
- r.note = why;
- return r;
- };
- constexpr int16_t CS = 10, DC = 11, RST = 12, BUSY = 13;
- Trace::set_dc_pin(DC);
- Peek<Driver> d(CS, DC, RST, BUSY);
- Trace::last_busy_release_value = d.busy_level() == HIGH ? LOW : HIGH;
- d.selectSPI(SPI, SPISettings());
- d.init(0);
- const uint16_t W = Driver::WIDTH, H = Driver::HEIGHT;
- const uint32_t plane_bytes = (uint32_t) W * H / 8; // 1bpp-sized source buffer
- auto *buf = new uint8_t[plane_bytes];
- auto burst_for = [&](uint8_t value) -> std::vector<uint8_t> {
- memset(buf, value, plane_bytes);
- Trace::start_event_record();
- d.writeImage(buf, buf, 0, 0, W, H);
- Trace::stop_event_record();
- return burst_data_after(group(0, Trace::events.size()), p0cmd);
- };
- const std::vector<uint8_t> white = burst_for(0xFF); // engine convention: bit=1 => white
- const std::vector<uint8_t> black = burst_for(0x00);
- delete[] buf;
- if (white.empty() || black.empty())
- return fail("bw-packed probe: no plane-write burst found");
- if (white.size() != black.size())
- return fail("bw-packed probe: white/black burst lengths differ");
- const uint32_t total_pixels = (uint32_t) W * H;
- if (white.size() == 0 || total_pixels % white.size() != 0)
- return fail("bw-packed probe: pixel count not a multiple of the wire-byte count");
- const int ppb = (int) (total_pixels / white.size()); // pixels per wire byte
- if (ppb <= 0 || ppb > 8 || (8 % ppb) != 0)
- return fail("bw-packed probe: implied pixels/byte doesn't divide 8 evenly");
- const int bpp = 8 / ppb;
- const uint8_t wbyte = white[0], bbyte = black[0];
- for (uint8_t v : white)
- if (v != wbyte)
- return fail("bw-packed probe: white-fill wire byte not uniform (packing depends on more than one bit)");
- for (uint8_t v : black)
- if (v != bbyte)
- return fail("bw-packed probe: black-fill wire byte not uniform (packing depends on more than one bit)");
- if (wbyte == bbyte)
- return fail("bw-packed probe: white and black fills produced the same wire byte");
- const uint8_t mask = (uint8_t) ((1u << bpp) - 1);
- const uint8_t white_code = (wbyte >> (bpp * (ppb - 1))) & mask; // MSB field (every field equal, uniform byte)
- const uint8_t black_code = (bbyte >> (bpp * (ppb - 1))) & mask;
- r.resolved = true;
- r.bits_per_pixel = bpp;
- r.color_count = 2;
- r.two_code_bw = true;
- r.blank_code = white_code;
- r.black_code = black_code;
- char note[96];
- snprintf(note, sizeof(note), "derived from raw 1bpp fills, no colour wrapper: black=0x%02X white=0x%02X, %d px/wire-byte",
- black_code, white_code, ppb);
- r.note = note;
- return r;
- };
- }
- // Plane-sized (>= PLANE_BURST_MIN) burst commands one call makes, from a
- // fresh instance with an untraced writeScreenBuffer() first (same trick
- // probe_plane_invert_values uses) to absorb the lazy _InitDisplay()/
- // _initial_write clear. Shared by probe_full_refresh_extra_write_cmd below so
- // it can tell a genuinely NEW burst from one the plain single-bitmap
- // writeImage() already makes on its own - a 2-plane (3-colour) driver's
- // single-bitmap writeImage(bitmap,...) forwards to writeImage(black, NULL,
- // ...), which bursts to BOTH its real plane_write_cmd[0] (real data) AND
- // plane_write_cmd[1] (a NULL-color blank-fill burst, same size) every time,
- // with no full-refresh involved at all - diffing against just plane_write_cmd
- // [0] would misread that second plane's ordinary burst as a full-refresh-only
- // extra write.
- template<typename Driver, typename Call> std::set<uint8_t> plane_burst_cmds(Call call) {
- constexpr int16_t CS = 10, DC = 11, RST = 12, BUSY = 13;
- Trace::set_dc_pin(DC);
- Peek<Driver> d(CS, DC, RST, BUSY);
- Trace::last_busy_release_value = d.busy_level() == HIGH ? LOW : HIGH;
- d.selectSPI(SPI, SPISettings());
- d.init(0);
- d.writeScreenBuffer(); // untraced: absorb lazy init + the _initial_write clear
- const uint32_t plane_bytes = (uint32_t) Driver::WIDTH * Driver::HEIGHT / 8;
- auto *buf = new uint8_t[plane_bytes];
- memset(buf, 0xA5, plane_bytes);
- Trace::start_event_record();
- call(d, buf);
- Trace::stop_event_record();
- delete[] buf;
- std::set<uint8_t> cmds;
- for (const auto &c : group(0, Trace::events.size()))
- if (c.data.size() >= PLANE_BURST_MIN)
- cmds.insert(c.cmd);
- return cmds;
- }
- // Detects a controller whose writeImageForFullRefresh() diverges from plain
- // writeImage() - e.g. SSD1680/SSD1619-family controllers write the same
- // bitmap to a "previous RAM" bank (a different command) before the normal
- // "current RAM" write plane_write_cmd[plane] already captures. GxEPD2_EPD's
- // base class defaults writeImageForFullRefresh() to call writeImage()
- // unchanged, so this probe generalizes across every controller without
- // hardcoding the known-affected families: it diffs the two calls' burst-
- // command sets and returns whatever writeImageForFullRefresh() adds. Returns
- // 0x00 (no divergence - by far the common case) or the extra command byte.
- template<typename Driver> uint8_t probe_full_refresh_extra_write_cmd() {
- auto write_cmds = plane_burst_cmds<Driver>(
- [](Peek<Driver> &d, uint8_t *buf) { d.writeImage(buf, 0, 0, Driver::WIDTH, Driver::HEIGHT); });
- auto full_cmds = plane_burst_cmds<Driver>(
- [](Peek<Driver> &d, uint8_t *buf) { d.writeImageForFullRefresh(buf, 0, 0, Driver::WIDTH, Driver::HEIGHT); });
- for (uint8_t c : full_cmds)
- if (!write_cmds.count(c))
- return c;
- return 0x00;
- }
- // Prints just the plane_write_cmd_for_full_refresh{} line (for splicing into
- // an existing hand-maintained panel_*.h, same workflow as
- // print_power_sequences / tools/capture.py --power). Self-contained like
- // print_power_sequences: derives p0cmd itself (the first plane-sized burst a
- // plain writeImage() produces) rather than trusting a shipped panel_*.h's
- // plane_write_cmd, which may have been hand-adjusted since it was captured.
- template<typename Driver> void print_full_refresh_extra_write() {
- constexpr int16_t CS = 10, DC = 11, RST = 12, BUSY = 13;
- Trace::set_dc_pin(DC);
- Peek<Driver> probe(CS, DC, RST, BUSY);
- Trace::last_busy_release_value = probe.busy_level() == HIGH ? LOW : HIGH;
- probe.selectSPI(SPI, SPISettings());
- probe.init(0);
- probe.writeScreenBuffer(); // untraced: absorb lazy init + the _initial_write clear
- const uint32_t plane_bytes = (uint32_t) Driver::WIDTH * Driver::HEIGHT / 8;
- auto *buf = new uint8_t[plane_bytes];
- memset(buf, 0xFF, plane_bytes); // 0xFF/0x00, not a filler byte: doubles as the 1bpp-vs-packed check below
- Trace::start_event_record();
- probe.writeImage(buf, 0, 0, Driver::WIDTH, Driver::HEIGHT);
- Trace::stop_event_record();
- delete[] buf;
- uint8_t p0cmd = 0x00;
- int w0_white = -1;
- for (const auto &c : group(0, Trace::events.size()))
- if (c.data.size() >= PLANE_BURST_MIN) {
- p0cmd = c.cmd;
- w0_white = c.data[0];
- break;
- }
- // A packed (>1bpp) driver's writeImageForFullRefresh expects its own packed
- // bit depth, not the 1bpp-sized buffer above - probing it the same way a
- // 1bpp panel is probed can over/under-read and crash (seen on GDEP073E01,
- // 4bpp 7-colour). A 1bpp panel's white-fill wire byte is always 0x00 or
- // 0xFF (see probe_plane_invert_values); anything else means packed, so bail
- // instead of guessing - this field isn't meaningful there anyway (see
- // emit_descriptor_impl's pv.is_1bpp gate).
- if (w0_white != 0x00 && w0_white != 0xFF) {
- printf(" // SKIPPED: packed (>1bpp) panel - writeImageForFullRefresh's dual-write behaviour is "
- "specific to the 1bpp plane model, not applicable here\n");
- return;
- }
- const uint8_t extra = probe_full_refresh_extra_write_cmd<Driver>();
- if (extra)
- printf(" .plane_write_cmd_for_full_refresh = {0x%02X, 0x00}, // writeImageForFullRefresh also "
- "re-writes this \"previous RAM\" bank before the 0x%02X write above - captured for future "
- "partial-refresh use, not read by this full-refresh-only engine yet\n",
- extra, p0cmd);
- else
- printf(" // writeImageForFullRefresh == writeImage for this controller: no "
- ".plane_write_cmd_for_full_refresh entry needed (0x00 default already correct)\n");
- }
- // Captures powerOff() or hibernate() as its own standalone command sequence -
- // NOT folded into the refresh_seq phase-segmentation above, and NOT captured
- // from the same live session as each other. Every GxEPD2 driver's powerOff()
- // guards on `_power_is_on` (set true by the refresh() a normal update ends
- // with), and hibernate() calls powerOff() again internally - so capturing
- // both from one session would have the second call's powerOff() portion
- // silently emit nothing (already off from the first). Each call here gets its
- // own fresh instance, replayed through the exact init/clear/write/refresh
- // setup the main capture above uses, so `call` always runs with
- // _power_is_on true, matching a real update-then-power-down.
- template<typename Driver, typename MethodCall> std::vector<Cmd> capture_power_sequence(MethodCall call) {
- constexpr int16_t CS = 10, DC = 11, RST = 12, BUSY = 13;
- Trace::set_dc_pin(DC);
- Peek<Driver> d(CS, DC, RST, BUSY);
- Trace::last_busy_release_value = d.busy_level() == HIGH ? LOW : HIGH;
- d.selectSPI(SPI, SPISettings());
- d.init(0);
- const uint32_t plane_bytes = (uint32_t) Driver::WIDTH * Driver::HEIGHT / 8;
- auto *buf = new uint8_t[plane_bytes];
- memset(buf, 0xA5, plane_bytes);
- d.writeScreenBuffer();
- d.writeImage(buf, buf, 0, 0, Driver::WIDTH, Driver::HEIGHT);
- d.refresh(false);
- delete[] buf;
- Trace::start_event_record();
- call(d);
- Trace::stop_event_record();
- return group(0, Trace::events.size());
- }
- // (power_off_sequence, hibernate_sequence) for Driver, each captured via
- // capture_power_sequence above. hibernate_sequence is hibernate()'s tail
- // PAST whatever it shares with powerOff() - every driver observed so far
- // calls powerOff() internally then adds exactly one more command (the
- // deep-sleep trigger) - since power_off_sequence already has that shared
- // part and CycleEngine runs the two back to back (see epd_command.h); a
- // prefix mismatch (seen nowhere so far) falls back to the full hibernate
- // capture rather than guess.
- template<typename Driver> std::pair<std::vector<Cmd>, std::vector<Cmd>> capture_power_off_and_hibernate() {
- std::vector<Cmd> power_off_seq = capture_power_sequence<Driver>([](Peek<Driver> &d) { d.powerOff(); });
- std::vector<Cmd> hibernate_full_seq = capture_power_sequence<Driver>([](Peek<Driver> &d) { d.hibernate(); });
- std::vector<Cmd> hibernate_seq;
- if (hibernate_full_seq.size() > power_off_seq.size()) {
- bool prefix_matches = true;
- for (size_t i = 0; i < power_off_seq.size() && prefix_matches; i++)
- if (hibernate_full_seq[i].cmd != power_off_seq[i].cmd || hibernate_full_seq[i].data != power_off_seq[i].data)
- prefix_matches = false;
- if (prefix_matches)
- hibernate_seq.assign(hibernate_full_seq.begin() + power_off_seq.size(), hibernate_full_seq.end());
- }
- if (hibernate_seq.empty())
- hibernate_seq = hibernate_full_seq;
- return {power_off_seq, hibernate_seq};
- }
- // Prints just the POWER_OFF/HIBERNATE arrays + EpdCommand tables (plus the
- // descriptor-field lines to paste) for Driver - for splicing
- // power_off_sequence/hibernate_sequence into an EXISTING, hand-maintained
- // panel_*.h (one shipped before these fields existed) without regenerating,
- // and losing, everything else already in it. tools/capture.py --power.
- template<typename Driver> void print_power_sequences(const char *macro) {
- auto captured = capture_power_off_and_hibernate<Driver>();
- const std::vector<Cmd> &power_off_seq = captured.first;
- const std::vector<Cmd> &hibernate_seq = captured.second;
- auto emit_seq = [&](const char *seqsuffix, const std::vector<Cmd> &cmds) {
- for (size_t i = 0; i < cmds.size(); i++)
- if (!cmds[i].data.empty()) {
- char suf[64];
- snprintf(suf, sizeof(suf), "%s_%zu_%02X", seqsuffix, i, cmds[i].cmd);
- print_data_array(macro, suf, cmds[i].data);
- }
- printf("\ninline const EpdCommand %s_%s[] = {\n", macro, seqsuffix);
- for (size_t i = 0; i < cmds.size(); i++) {
- char suf[64];
- snprintf(suf, sizeof(suf), "%s_%zu_%02X", seqsuffix, i, cmds[i].cmd);
- print_cmd(macro, suf, cmds[i]);
- }
- printf("};\n");
- };
- if (!power_off_seq.empty()) {
- emit_seq("POWER_OFF", power_off_seq);
- printf("// splice into the descriptor:\n");
- printf("// .power_off_sequence = %s_POWER_OFF,\n", macro);
- printf("// .power_off_sequence_len = sizeof(%s_POWER_OFF) / sizeof(%s_POWER_OFF[0]),\n", macro, macro);
- } else {
- printf("// powerOff(): empty capture - this driver's full refresh already leaves\n"
- "// _power_is_on false (its _Update_Full() powers itself down), so powerOff()\n"
- "// has nothing left to do. No power_off_sequence to add.\n");
- }
- printf("\n");
- if (!hibernate_seq.empty()) {
- emit_seq("HIBERNATE", hibernate_seq);
- printf("// splice into the descriptor:\n");
- printf("// .hibernate_sequence = %s_HIBERNATE,\n", macro);
- printf("// .hibernate_sequence_len = sizeof(%s_HIBERNATE) / sizeof(%s_HIBERNATE[0]),\n", macro, macro);
- } else {
- printf("// hibernate(): empty capture (unexpected - every driver seen so far adds at\n"
- "// least the deep-sleep trigger when rst is wired). Review by hand.\n");
- }
- }
- template<typename Driver>
- void emit_descriptor_impl(const char *class_name, const char *macro, const PackedResolverFn &resolve_packed) {
- constexpr int16_t CS = 10, DC = 11, RST = 12, BUSY = 13;
- Trace::set_dc_pin(DC);
- Peek<Driver> driver(CS, DC, RST, BUSY);
- const int bl = driver.busy_level();
- Trace::last_busy_release_value = bl == HIGH ? LOW : HIGH;
- driver.selectSPI(SPI, SPISettings());
- 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];
- for (uint32_t i = 0; i < plane_bytes; i++) {
- black[i] = 0xA5;
- color[i] = 0xA5;
- }
- Trace::start_event_record();
- const size_t s0 = 0;
- driver.writeScreenBuffer(); // init + clear burst
- const size_t s1 = Trace::events.size();
- driver.writeImage(black, color, 0, 0, Driver::WIDTH, Driver::HEIGHT); // window + plane
- const size_t s2 = Trace::events.size();
- driver.refresh(false); // window-again + trigger
- const size_t s3 = Trace::events.size();
- Trace::stop_event_record();
- delete[] black;
- delete[] color;
- // --- segment ---
- auto init_stage = group(s0, s1);
- auto image_stage = group(s1, s2);
- auto refresh_stage = group(s2, s3);
- auto cmds_equal = [](const std::vector<Cmd> &a, const std::vector<Cmd> &b) {
- if (a.size() != b.size())
- return false;
- for (size_t i = 0; i < a.size(); i++)
- if (a[i].cmd != b[i].cmd || a[i].data != b[i].data)
- return false;
- return true;
- };
- // Every pixel-plane burst in the image call. One => single-plane panel;
- // N => N planes (two colour planes sharing a region, or two physical
- // regions - told apart below by whether their window-sets match).
- std::vector<size_t> bursts;
- for (size_t i = 0; i < image_stage.size(); i++)
- if (image_stage[i].data.size() >= PLANE_BURST_MIN)
- bursts.push_back(i);
- const size_t nplanes = bursts.empty() ? 1 : bursts.size();
- if (getenv("EMIT_DEBUG")) {
- fprintf(stderr, "image_stage %zu cmds; bursts:", image_stage.size());
- for (auto b : bursts)
- fprintf(stderr, " [%zu]=0x%02X", b, image_stage[b].cmd);
- fprintf(stderr, "\n");
- for (size_t i = 0; i < image_stage.size(); i++)
- fprintf(stderr, " [%zu] 0x%02X data=%zu\n", i, image_stage[i].cmd, image_stage[i].data.size());
- }
- // pre_block(k) = commands after burst k-1 (or the start) up to, but not
- // including, burst k's own plane-write command - i.e. the window set for
- // plane k, possibly with the lazily-run _InitDisplay in front for k==0.
- auto pre_block = [&](size_t k) {
- size_t lo = (k == 0) ? 0 : bursts[k - 1] + 1;
- return std::vector<Cmd>(image_stage.begin() + lo, image_stage.begin() + bursts[k]);
- };
- std::vector<Cmd> init_seq, window0, window1;
- std::vector<uint8_t> plane_cmds;
- bool per_plane = false;
- bool dual_region = false; // two physical regions (per-region select + window)
- int plane_width_val = 0; // decoded per-region width (dual_region only)
- uint32_t region0_burst = 0; // region 0's image burst length, for line-order detection
- if (bursts.size() >= 2) {
- // Multi-plane. Two observed layouts:
- // interleaved: [init][W0][burst0][W1][burst1] - each plane's window
- // sits between its burst and the previous one (dual region);
- // batched: [init][W][W][burst0][burst1] - all windows precede all
- // bursts (SSD1680 3-colour: writeImage sets the black and
- // red RAM windows, then bursts both planes).
- for (size_t k = 0; k < bursts.size(); k++)
- plane_cmds.push_back(image_stage[bursts[k]].cmd);
- const size_t n = bursts.size();
- std::vector<Cmd> pre = pre_block(0);
- std::vector<std::vector<Cmd>> windows;
- bool interleaved = false;
- for (size_t k = 1; k < n; k++)
- if (bursts[k] - bursts[k - 1] > 1) {
- interleaved = true;
- break;
- }
- if (interleaved) {
- std::vector<Cmd> gap0(image_stage.begin() + bursts[0] + 1, image_stage.begin() + bursts[1]);
- const size_t wl = gap0.size();
- if (wl <= pre.size()) {
- windows.push_back(std::vector<Cmd>(pre.end() - wl, pre.end()));
- init_seq.assign(pre.begin(), pre.end() - wl);
- } else {
- windows.push_back({});
- init_seq = pre;
- }
- for (size_t k = 1; k < n; k++)
- windows.push_back(std::vector<Cmd>(image_stage.begin() + bursts[k - 1] + 1, image_stage.begin() + bursts[k]));
- } else {
- // Batched: pre = init + n copies of the window. Peel the smallest block
- // that repeats n times at the tail; the remainder is init.
- size_t L = 0;
- for (size_t cand = 1; cand * n <= pre.size(); cand++) {
- bool ok = true;
- for (size_t b = 0; b < n && ok; b++)
- for (size_t j = 0; j < cand && ok; j++) {
- const Cmd &a = pre[pre.size() - cand * n + b * cand + j];
- const Cmd &c = pre[pre.size() - cand + j];
- if (a.cmd != c.cmd || a.data != c.data)
- ok = false;
- }
- if (ok) {
- L = cand;
- break;
- }
- }
- if (L > 0) {
- for (size_t b = 0; b < n; b++)
- windows.push_back(std::vector<Cmd>(pre.end() - L * (n - b), pre.end() - L * (n - b - 1)));
- init_seq.assign(pre.begin(), pre.end() - L * n);
- } else {
- windows.push_back({});
- init_seq = pre;
- }
- }
- window0 = windows.empty() ? std::vector<Cmd>{} : windows[0];
- window1 = windows.size() > 1 ? windows[1] : std::vector<Cmd>{};
- for (size_t k = 1; k < windows.size(); k++)
- if (!cmds_equal(windows[0], windows[k]))
- per_plane = true; // per-plane windows differ => separate regions
- // Dual-region refinement (e.g. GDEY0579F51 / HX8717: a two-chip panel whose
- // "planes" are two physical half-width regions, each gated by a region-
- // select command before its own window + burst). The generic interleaved
- // path above already recovers the per-region windows and plane commands;
- // three things it can't are filled here.
- if (interleaved && per_plane) {
- dual_region = true;
- std::set<uint8_t> wops; // region-select + window-set opcodes
- for (const auto &c : window0)
- wops.insert(c.cmd);
- for (const auto &c : window1)
- wops.insert(c.cmd);
- // (1) INIT. A dual-region panel runs its whole _InitDisplay during the
- // earlier writeScreenBuffer clear, and that init REUSES the region-select
- // opcode (select master/slave for each region's PSR), so the single-plane
- // "cut at the first window opcode" rule would cut at the very first init
- // command. Instead take init from the clear stage and drop only the
- // CONTIGUOUS trailing run of window-set commands before the first clear
- // burst (the both-/first-region window-sets); the select/PSR pairs earlier
- // in init survive because a non-window command (BTST, power-on, ...)
- // separates them from that trailing run.
- const size_t cb = plane_index(init_stage); // first clear burst
- size_t cut = cb;
- while (cut > 0 && wops.count(init_stage[cut - 1].cmd))
- cut--;
- init_seq.assign(init_stage.begin(), init_stage.begin() + cut);
- // (2) plane_width: each region addresses its own half. Decode it from the
- // window-set command's payload - first two big-endian 16-bit fields are
- // x and x_end, as every window encoder observed so far lays them out.
- for (const auto &c : window0)
- if (c.data.size() >= 4) {
- int x = (c.data[0] << 8) | c.data[1];
- int xe = (c.data[2] << 8) | c.data[3];
- plane_width_val = xe - x + 1;
- break;
- }
- // (3) line doubling: region 0's burst length, compared in the emit stage
- // against plane_width*height*bpp/8 to flag PAIRED_EDGES.
- region0_burst = (uint32_t) image_stage[bursts[0]].data.size();
- }
- } else {
- // Single plane: window = the image call's pre-burst block; init comes
- // from the writeScreenBuffer stage, cut at the first window-set command
- // (SSD16xx sets the window there too - that belongs in window_sequence).
- const size_t wp = plane_index(image_stage);
- window0.assign(image_stage.begin(), image_stage.begin() + wp);
- std::set<uint8_t> wops;
- for (const auto &c : window0)
- wops.insert(c.cmd);
- const size_t ip = plane_index(init_stage);
- size_t cut = ip;
- for (size_t i = 0; i < ip; i++)
- if (wops.count(init_stage[i].cmd)) {
- cut = i;
- break;
- }
- init_seq.assign(init_stage.begin(), init_stage.begin() + cut);
- plane_cmds.push_back(wp < image_stage.size() ? image_stage[wp].cmd : 0x00);
- }
- // blank/clear fill byte, observed from the writeScreenBuffer clear burst.
- const size_t cip = plane_index(init_stage);
- const uint8_t clear_fill = (cip < init_stage.size() && !init_stage[cip].data.empty()) ? init_stage[cip].data[0] : 0x00;
- // plane_invert + 1bpp/packed detection, derived by the fill-capture probe.
- const uint8_t p0cmd = plane_cmds.empty() ? 0x00 : plane_cmds[0];
- const uint8_t p1cmd = plane_cmds.size() > 1 ? plane_cmds[1] : 0x00;
- const PlaneInvert pv = probe_plane_invert_values<Driver>(p0cmd, p1cmd, nplanes);
- // Packed-family resolution (4-colour palette etc.), run once here so both the
- // busy-timeout class and the packed fields below use the same answer. 1bpp
- // panels skip it. A specific colour-wrapper resolver (4-colour/7-colour,
- // passed in by emit_descriptor_4c/_7c) takes priority; absent one (the
- // default emit_descriptor<Driver> entry - a plain, possibly still-mono-
- // wrapped driver), fall back to the generic no-wrapper probe, which catches
- // a GxEPD2_BW driver that packs internally (see make_resolver_bw_packed).
- // An unresolved packed panel (neither resolver recognised it) is treated as
- // the slow class for the timeout floor.
- const PackedResolverFn effective_resolver = resolve_packed ? resolve_packed : make_resolver_bw_packed<Driver>(p0cmd);
- const PackedResolved pr = (!pv.is_1bpp && effective_resolver) ? effective_resolver(clear_fill, pv) : PackedResolved{};
- const int depth_bpp = pv.is_1bpp ? 1 : (pr.resolved ? pr.bits_per_pixel : 4);
- // Refresh = post-write commands after the LAST burst (e.g. UC8179's 0x92),
- // then the refresh call minus any window-set it repeats.
- std::set<uint8_t> all_wops;
- for (const auto &c : window0)
- all_wops.insert(c.cmd);
- for (const auto &c : window1)
- all_wops.insert(c.cmd);
- std::vector<Cmd> refresh_seq;
- if (!bursts.empty())
- for (size_t i = bursts.back() + 1; i < image_stage.size() && image_stage[i].data.size() < PLANE_BURST_MIN; i++)
- refresh_seq.push_back(image_stage[i]);
- // A shared-region panel re-sets its one window in refresh() and we drop that
- // duplicate; a dual-region panel's refresh instead sets a DISTINCT both-region
- // window (its own select payload), which is part of the refresh - keep it.
- for (const auto &c : refresh_stage)
- if (dual_region || !all_wops.count(c.cmd))
- refresh_seq.push_back(c);
- // The refresh ends at its trigger (the command we busy-wait on). A dual-region
- // driver re-sets the window AFTER the trigger to prime its own next writeImage;
- // our engine always sets its own window per cycle, so that trailing window is
- // dead in replay - trim anything past the last busy-wait.
- if (dual_region) {
- size_t last_wait = refresh_seq.size();
- for (size_t i = 0; i < refresh_seq.size(); i++)
- if (refresh_seq[i].wait_busy)
- last_wait = i;
- if (last_wait < refresh_seq.size())
- refresh_seq.resize(last_wait + 1);
- }
- // powerOff()/hibernate(), each its own fresh-session capture - see
- // capture_power_off_and_hibernate's doc comment.
- auto power_captured = capture_power_off_and_hibernate<Driver>();
- std::vector<Cmd> &power_off_seq = power_captured.first;
- std::vector<Cmd> &hibernate_seq = power_captured.second;
- // plane_write_cmd_for_full_refresh - computed here, BEFORE the "// GENERATED"
- // marker below, not alongside the printf()s that use it: every probe in this
- // function runs its own fresh Peek<Driver> through an un-traced init()/
- // writeScreenBuffer() first (see plane_burst_cmds), which prints raw
- // pinMode()/digitalWrite()/etc lines straight to stdout outside the
- // Trace::event_recording bracket - harmless for every OTHER probe above
- // because capture()/run_program() in tools/capture.py slice captured stdout
- // from the FIRST "// GENERATED" marker onward, dropping that noise. Calling
- // this probe any later - after the marker's printed - would leak its raw
- // trace lines into the middle of the emitted descriptor instead.
- const uint8_t full_refresh_extra = pv.is_1bpp ? probe_full_refresh_extra_write_cmd<Driver>() : 0x00;
- // --- emit ---
- printf("// GENERATED by emit_descriptor from %s - candidate.\n", class_name);
- 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("// 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("// 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");
- if (pr.resolved && pr.two_code_bw) {
- printf("// 2-code packed mono (see .bits_per_pixel below): derived purely by observing\n");
- printf("// raw white/black fills through the plane-write path, no colour wrapper to\n");
- printf("// drive (see make_resolver_bw_packed in emit_descriptor.h).\n");
- printf("inline const EpdPaletteEntry %s_PALETTE[] = {\n", macro);
- printf(" {0x00, 0x00, 0x00, 0x%02X}, // black\n", pr.black_code & 0xFF);
- printf(" {0xFF, 0xFF, 0xFF, 0x%02X}, // white\n", pr.blank_code & 0xFF);
- printf("};\n");
- printf("inline const uint8_t %s_PALETTE_LEN = sizeof(%s_PALETTE) / sizeof(%s_PALETTE[0]);\n\n", macro, macro, macro);
- }
- auto emit_seq = [&](const char *seqsuffix, std::vector<Cmd> &cmds) {
- for (size_t i = 0; i < cmds.size(); i++)
- if (!cmds[i].data.empty()) {
- char suf[64];
- snprintf(suf, sizeof(suf), "%s_%zu_%02X", seqsuffix, i, cmds[i].cmd);
- print_data_array(macro, suf, cmds[i].data);
- }
- printf("\ninline const EpdCommand %s_%s[] = {\n", macro, seqsuffix);
- for (size_t i = 0; i < cmds.size(); i++) {
- char suf[64];
- snprintf(suf, sizeof(suf), "%s_%zu_%02X", seqsuffix, i, cmds[i].cmd);
- print_cmd(macro, suf, cmds[i]);
- }
- printf("};\n\n");
- };
- const bool w0_empty = window0.empty(); // e.g. OTP-default panels (F52) set no window
- emit_seq("INIT", init_seq);
- if (!w0_empty)
- emit_seq("WINDOW_SEQ0", window0);
- if (per_plane)
- emit_seq("WINDOW_SEQ1", window1);
- emit_seq("REFRESH", refresh_seq);
- const bool has_power_off = !power_off_seq.empty();
- const bool has_hibernate = !hibernate_seq.empty();
- if (has_power_off)
- emit_seq("POWER_OFF", power_off_seq);
- if (has_hibernate)
- emit_seq("HIBERNATE", hibernate_seq);
- printf("inline const EpdPanelDescriptor %s = {\n", macro);
- printf(" .width = %u,\n", (unsigned) Driver::WIDTH);
- printf(" .height = %u,\n", (unsigned) Driver::HEIGHT);
- printf(" .busy_active_level = %s,\n", bl == HIGH ? "true" : "false");
- const char *depth_class = depth_bpp >= 4 ? "4bpp 7-colour" : depth_bpp == 2 ? "2bpp 4-colour" : "1bpp mono/3-colour";
- printf(" .busy_timeout_ms = %u, // generous diagnostic ceiling (vendor full_refresh_time %ums, "
- "%s class); dev-tunable\n",
- (unsigned) default_busy_timeout_ms(Driver::full_refresh_time, depth_bpp),
- (unsigned) Driver::full_refresh_time, depth_class);
- printf(" .init_sequence = %s_INIT,\n", macro);
- printf(" .init_sequence_len = sizeof(%s_INIT) / sizeof(%s_INIT[0]),\n", macro, macro);
- if (per_plane) {
- printf(" .window_sequence = {%s_WINDOW_SEQ0, %s_WINDOW_SEQ1},\n", macro, macro);
- printf(" .window_sequence_len = {sizeof(%s_WINDOW_SEQ0) / sizeof(%s_WINDOW_SEQ0[0]), "
- "sizeof(%s_WINDOW_SEQ1) / sizeof(%s_WINDOW_SEQ1[0])},\n",
- macro, macro, macro, macro);
- printf(" .window_per_plane = true, // per-plane windows differ => separate regions\n");
- } else if (w0_empty) {
- printf(" .window_sequence = {nullptr, nullptr}, // panel sets no window (OTP-default frame)\n");
- printf(" .window_sequence_len = {0, 0},\n");
- } else {
- printf(" .window_sequence = {%s_WINDOW_SEQ0, nullptr},\n", macro);
- printf(" .window_sequence_len = {sizeof(%s_WINDOW_SEQ0) / sizeof(%s_WINDOW_SEQ0[0]), 0},\n", macro, macro);
- }
- printf(" .plane_count = %zu,\n", nplanes);
- printf(" .plane_write_cmd = {0x%02X, 0x%02X},\n", p0cmd, p1cmd);
- // full_refresh_extra was computed above, before the "// GENERATED" marker -
- // see that computation's comment for why. A packed (>1bpp) driver's
- // writeImageForFullRefresh takes the same single-bitmap buffer shape, but
- // its _writeImage expects a buffer already packed to its own bit depth -
- // probing it with a 1bpp-sized buffer over/under-reads and crashes some
- // drivers (e.g. GDEP073E01, 4bpp 7-colour), hence the pv.is_1bpp gate above;
- // packed panels just keep the {0,0} default.
- if (full_refresh_extra)
- printf(" .plane_write_cmd_for_full_refresh = {0x%02X, 0x00}, // writeImageForFullRefresh also "
- "re-writes this \"previous RAM\" bank before the 0x%02X write above - not read by this "
- "full-refresh-only engine yet, captured for future partial-refresh use\n",
- full_refresh_extra, p0cmd);
- // Fields below are emitted in struct-declaration order (plane_invert,
- // plane_width, line_order, bits_per_pixel, palette, palette_len,
- // blank_code) so the designated initializer stays well-formed.
- if (pv.is_1bpp) {
- const bool i1 = (nplanes > 1 && pv.plane1_valid) ? pv.inv1 : false;
- printf(" .plane_invert = {%s, %s}, // probe-derived\n", pv.inv0 ? "true" : "false", i1 ? "true" : "false");
- } else {
- printf(" .plane_invert = {false, false}, // unused: packed panel\n");
- }
- if (per_plane) {
- if (dual_region && plane_width_val > 0)
- printf(" .plane_width = %d, // decoded from the region window-set (x_end-x+1); sanity-check\n",
- plane_width_val);
- else
- printf(" .plane_width = 0, // TODO: diag pass (separate-region width)\n");
- }
- // line_order: a dual-region panel whose per-region burst is twice the region's
- // own pixel bytes is sending two wire lines per row (GxEPD2 writeNative's
- // doubled, both-edges-to-middle emission). The doubling is observable here;
- // the exact row order that PAIRED_EDGES encodes is NOT (a uniform fill can't
- // show which buffer row each line carries), so emit it as a candidate the
- // diag native-path pass / driver source confirms.
- if (dual_region && plane_width_val > 0 && depth_bpp > 0) {
- uint32_t expect = (uint32_t) plane_width_val * Driver::HEIGHT * depth_bpp / 8;
- if (expect && region0_burst == 2u * expect)
- printf(" .line_order = LineOrder::PAIRED_EDGES, // region burst %u B = 2x %u B (2 lines/row); "
- "confirm edges-to-middle order via the diag native-path pass\n",
- region0_burst, expect);
- else if (expect && region0_burst != expect)
- printf(" // NOTE: region burst %u B vs expected %u B (1 line/row) - line order unclear, "
- "check with the diag native-path pass\n",
- region0_burst, expect);
- }
- if (pv.is_1bpp) {
- // 1bpp plane model: depth confirmed by the probe; palette/blank_code unused.
- printf(" .bits_per_pixel = 1, // probe: white fill => all-bits wire byte 0x%02X\n", pv.w0_white & 0xFF);
- printf(" .palette = nullptr,\n");
- printf(" .palette_len = 0,\n");
- } else {
- // Packed panel. If a family resolver recognised it (4-colour, via
- // emit_palette_4c.h), it derived depth/palette/blank_code by observing the
- // 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
- // family, e.g. a packed panel outside the 4-colour/7-colour resolvers).
- if (pr.resolved && pr.two_code_bw) {
- printf(" .bits_per_pixel = %d, // probe: %s\n", pr.bits_per_pixel, pr.note.c_str());
- printf(" .palette = %s_PALETTE,\n", macro);
- printf(" .palette_len = %s_PALETTE_LEN,\n", macro);
- printf(" .blank_code = 0x%02X, // observed white-fill code\n", pr.blank_code & 0xFF);
- } else 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,
- shown_colors, pr.note.empty() ? "" : " - ", pr.note.c_str());
- printf(" .palette = %s,\n", pr.palette_expr);
- printf(" .palette_len = %s,\n", pr.palette_len_expr);
- printf(" .blank_code = 0x%02X, // observed white fill code\n", pr.blank_code & 0xFF);
- } else {
- printf(" .bits_per_pixel = 2, // TODO: diag pass - PACKED (white fill wire 0x%02X, not 1bpp)%s%s\n",
- pv.w0_white & 0xFF, pr.note.empty() ? "" : " - ", pr.note.c_str());
- printf(" .palette = nullptr, // TODO: diag pass\n");
- printf(" .palette_len = 0,\n");
- printf(" .blank_code = 0x00, // TODO: diag pass (clear fill observed 0x%02X)\n", clear_fill);
- }
- }
- printf(" .refresh_sequence = %s_REFRESH,\n", macro);
- printf(" .refresh_sequence_len = sizeof(%s_REFRESH) / sizeof(%s_REFRESH[0]),\n", macro, macro);
- if (has_power_off) {
- printf(" .power_off_sequence = %s_POWER_OFF, // probe: powerOff(), fresh session after a normal refresh\n", macro);
- printf(" .power_off_sequence_len = sizeof(%s_POWER_OFF) / sizeof(%s_POWER_OFF[0]),\n", macro, macro);
- }
- if (has_hibernate) {
- printf(" .hibernate_sequence = %s_HIBERNATE, // probe: hibernate()'s tail past powerOff() (see file header)\n",
- macro);
- printf(" .hibernate_sequence_len = sizeof(%s_HIBERNATE) / sizeof(%s_HIBERNATE[0]),\n", macro, macro);
- }
- printf("};\n\n} // namespace esphome::gxepd2\n");
- }
- // Generic entry: no packed-family resolver, so a packed panel's
- // depth/palette/blank_code emit as TODO placeholders (the pre-resolver
- // behaviour). The 4-colour entry is emit_descriptor_4c() in emit_palette_4c.h.
- template<typename Driver> void emit_descriptor(const char *class_name, const char *macro) {
- emit_descriptor_impl<Driver>(class_name, macro, nullptr);
- }
- } // namespace emit
|