emit_descriptor.h 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005
  1. #pragma once
  2. // Prototype: emit a candidate panel_*.h EpdPanelDescriptor by *observing* a
  3. // real vendor GxEPD2 driver run, instead of hand-transcribing its C++. This
  4. // closes the hand-derivation gap for the straight-line parts of a descriptor
  5. // (init / window / refresh command sequences + the plane-write command) by
  6. // phase-segmenting the captured event stream at the public-API boundary - no
  7. // edits to the vendor source.
  8. //
  9. // Staging (the trick that separates the phases without instrumenting the
  10. // vendor's private methods):
  11. // writeScreenBuffer() -> lazy _InitDisplay() + a plane-write burst (clear).
  12. // Everything before that burst is the INIT sequence;
  13. // the command right before it is the PLANE-WRITE cmd.
  14. // writeImage(pattern) -> init already done, so this is just the WINDOW set
  15. // (commands before the pattern burst) + plane write.
  16. // refresh() -> re-sets the window then triggers; dropping the
  17. // duplicate window command leaves the REFRESH seq.
  18. //
  19. // Out of scope here (comes from the diag Pattern A/B passes, not one capture):
  20. // bits_per_pixel / palette / plane_invert / line_order. Those are emitted
  21. // as TODO placeholders so the diff against the hand file is about segmentation.
  22. #include <cstdint>
  23. #include <cstdio>
  24. #include <cstdlib>
  25. #include <functional>
  26. #include <set>
  27. #include <string>
  28. #include <type_traits>
  29. #include <utility>
  30. #include <vector>
  31. #include "spy/Arduino.h"
  32. #include "spy/SPI.h"
  33. #include "trace.h"
  34. namespace emit {
  35. // A data burst at least this long is a pixel-plane write, not config data.
  36. constexpr size_t PLANE_BURST_MIN = 256;
  37. struct Cmd {
  38. uint8_t cmd;
  39. std::vector<uint8_t> data;
  40. bool wait_busy = false;
  41. unsigned long delay_after = 0;
  42. };
  43. // Expose GxEPD2_EPD's protected busy level without parsing (same trick as
  44. // run_harness.h's Peek).
  45. template<typename Driver> struct Peek : Driver {
  46. using Driver::Driver;
  47. int busy_level() const { return this->_busy_level; }
  48. };
  49. // Group a slice of the event stream into EpdCommand-shaped records. Events
  50. // before the first SPI *command* (the reset pulse + its delays) are skipped -
  51. // the engine owns the hardware reset, so it never belongs in a descriptor.
  52. inline std::vector<Cmd> group(size_t lo, size_t hi) {
  53. std::vector<Cmd> cmds;
  54. bool started = false;
  55. for (size_t i = lo; i < hi; i++) {
  56. const auto &e = Trace::events[i];
  57. switch (e.kind) {
  58. case EventKind::SPI_TRANSFER:
  59. if (e.is_command) {
  60. cmds.push_back(Cmd{e.byte});
  61. started = true;
  62. } else if (started) {
  63. cmds.back().data.push_back(e.byte);
  64. }
  65. break;
  66. case EventKind::DELAY:
  67. if (started)
  68. cmds.back().delay_after += e.ms;
  69. break;
  70. case EventKind::DIGITAL_READ: // the only reads are busy-pin polls
  71. if (started) {
  72. cmds.back().wait_busy = true;
  73. // The delay(1) GxEPD2_EPD::_waitWhileBusy() runs inside its poll
  74. // loop lands right before this read and is part of the busy-wait,
  75. // not an inter-command delay - our engine owns busy-waiting, so
  76. // fold it in rather than emit it. (This also absorbs a vendor's own
  77. // tiny settle delay right before a refresh trigger, e.g. 290c's
  78. // _refresh delay(1); immaterial, the panel asserts busy regardless.)
  79. cmds.back().delay_after = 0;
  80. }
  81. break;
  82. default:
  83. break; // DIGITAL_WRITE = reset/CS/DC toggles: engine's job, not data
  84. }
  85. }
  86. return cmds;
  87. }
  88. // Index of the first command whose data burst marks a plane write, or np
  89. // (cmds.size()) if there's none in this slice.
  90. inline size_t plane_index(const std::vector<Cmd> &cmds) {
  91. for (size_t i = 0; i < cmds.size(); i++)
  92. if (cmds[i].data.size() >= PLANE_BURST_MIN)
  93. return i;
  94. return cmds.size();
  95. }
  96. inline void print_data_array(const char *macro, const char *suffix, const std::vector<uint8_t> &d) {
  97. printf("inline const uint8_t %s_%s[] = {", macro, suffix);
  98. for (size_t i = 0; i < d.size(); i++)
  99. printf("%s0x%02X", i ? ", " : "", d[i]);
  100. printf("};\n");
  101. }
  102. // Print one EpdCommand{} initializer line, with the optional fields only when
  103. // they're non-default (matches the hand files' style).
  104. inline void print_cmd(const char *macro, const char *suffix, const Cmd &c) {
  105. if (c.data.empty() && !c.wait_busy && c.delay_after == 0) {
  106. printf(" {0x%02X, nullptr, 0},\n", c.cmd);
  107. return;
  108. }
  109. const char *dataref = c.data.empty() ? "nullptr" : nullptr;
  110. if (dataref)
  111. printf(" {0x%02X, nullptr, 0", c.cmd);
  112. else
  113. printf(" {0x%02X, %s_%s, %u", c.cmd, macro, suffix, (unsigned) c.data.size());
  114. if (c.wait_busy || c.delay_after)
  115. printf(", /*delay_before_ms=*/0, /*delay_after_ms=*/%lu, /*wait_busy_after=*/%s", c.delay_after,
  116. c.wait_busy ? "true" : "false");
  117. printf("},\n");
  118. }
  119. // First data byte of the burst written right after command `op`, or -1.
  120. inline int burst_byte_after(const std::vector<Cmd> &cmds, uint8_t op) {
  121. for (const auto &c : cmds)
  122. if (c.cmd == op && c.data.size() >= PLANE_BURST_MIN)
  123. return c.data[0];
  124. return -1;
  125. }
  126. // The full data burst written right after command `op`, or empty.
  127. inline std::vector<uint8_t> burst_data_after(const std::vector<Cmd> &cmds, uint8_t op) {
  128. for (const auto &c : cmds)
  129. if (c.cmd == op && c.data.size() >= PLANE_BURST_MIN)
  130. return c.data;
  131. return {};
  132. }
  133. // Detects the two-arg writeScreenBuffer(black_value, color_value) that colour
  134. // (plane_count > 1) drivers have and mono drivers don't.
  135. template<typename T, typename = void> struct has_wsb2 : std::false_type {};
  136. template<typename T>
  137. struct has_wsb2<T, std::void_t<decltype(std::declval<T &>().writeScreenBuffer(uint8_t{}, uint8_t{}))>>
  138. : std::true_type {};
  139. struct PlaneInvert {
  140. bool inv0 = false, inv1 = false;
  141. bool plane1_valid = false;
  142. bool is_1bpp = false; // white fill => an all-bits-same wire byte (not packed)
  143. int w0_white = -1, w1_red = -1;
  144. };
  145. // A packed panel's depth / palette / blank code, resolved by observation for
  146. // the panel families the emitter knows how to probe (currently 4-colour, via
  147. // emit_palette_4c.h). `emit_descriptor_impl` calls a resolver of this shape
  148. // only for a packed panel (the plane-invert probe said white fill isn't an
  149. // all-bits wire byte); a null or unresolved result keeps the old TODO fields,
  150. // so an unknown packed family still emits a reviewable placeholder rather than
  151. // a wrong palette.
  152. struct PackedResolved {
  153. bool resolved = false; // false => emit TODO placeholders
  154. int bits_per_pixel = 2; // 2 (4-colour) or 4 (7-colour)
  155. const char *palette_expr = "nullptr"; // e.g. "GXEPD2_PALETTE_4C"
  156. const char *palette_len_expr = "0"; // e.g. "GXEPD2_PALETTE_4C_LEN"
  157. int blank_code = 0; // per-pixel clear code (observed white)
  158. int color_count = -1; // actual distinct colours probed; -1 => assume 1<<bits_per_pixel
  159. std::string note; // appended as a comment (observed table, or why unresolved)
  160. // Set by make_resolver_bw_packed (below): a still-GxEPD2_BW-wrapped (binary)
  161. // driver that nonetheless packs >1 bit/pixel on the wire internally, with no
  162. // colour wrapper to drive a la emit_palette_4c.h/7c.h - e.g. GxEPD2_583's
  163. // _send8pixel. Only two codes are ever observed (black_code, blank_code=
  164. // white code), so the emitter prints a local 2-entry palette array instead
  165. // of referencing a shared GXEPD2_PALETTE_* constant.
  166. bool two_code_bw = false;
  167. int black_code = -1;
  168. };
  169. using PackedResolverFn = std::function<PackedResolved(uint8_t clear_fill, const PlaneInvert &pv)>;
  170. // A generous, class-aware default for busy_timeout_ms. The timeout is only a
  171. // diagnostic ceiling - the engine hard-aborts a busy-wait that runs past it and
  172. // logs a failure, where GxEPD2 itself just keeps polling - so erring long costs
  173. // nothing on a healthy panel and the exact number is meant to be dev-tuned. The
  174. // vendor full_refresh_time is a soft estimate, so we take 2x it, then clamp up
  175. // to a per-depth floor so a panel that under-reports (or whose colour pass is
  176. // genuinely slow) still gets room: mono/3-colour fail fast, 4-colour and
  177. // 7-colour (ACeP) get progressively more headroom.
  178. inline uint32_t default_busy_timeout_ms(unsigned full_refresh_time, int bits_per_pixel) {
  179. uint32_t headroom = static_cast<uint32_t>(full_refresh_time) * 2;
  180. uint32_t floor = bits_per_pixel >= 4 ? 60000u : bits_per_pixel == 2 ? 45000u : 15000u;
  181. return headroom > floor ? headroom : floor;
  182. }
  183. // plane_invert by capture (see probe_plane_invert.h for the rationale): drive
  184. // known full-screen fills and read each plane's wire byte, reconciling against
  185. // the engine's fixed buffer convention (plane0 bit=white, plane1 bit=red). A
  186. // white-fill plane0 wire that isn't 0x00/0xFF means a packed (>1bpp) panel,
  187. // where plane_invert is unused and bits_per_pixel/palette come from the diag
  188. // pass instead.
  189. template<typename Driver> PlaneInvert probe_plane_invert_values(uint8_t p0cmd, uint8_t p1cmd, size_t plane_count) {
  190. constexpr int16_t CS = 10, DC = 11, RST = 12, BUSY = 13;
  191. Trace::set_dc_pin(DC);
  192. Peek<Driver> d(CS, DC, RST, BUSY);
  193. Trace::last_busy_release_value = d.busy_level() == HIGH ? LOW : HIGH;
  194. d.selectSPI(SPI, SPISettings());
  195. d.init(0);
  196. // plane_bytes sized for the 1bpp hypothesis - same assumption
  197. // emit_descriptor_impl's own main capture makes (see its `black`/`color`
  198. // buffers). A genuinely packed driver still re-packs this buffer through
  199. // its real plane-write path (e.g. GxEPD2_583's _send8pixel), so the wire
  200. // byte it reports is the true packed code, not a false 1bpp reading.
  201. const uint32_t plane_bytes = (static_cast<uint32_t>(Driver::WIDTH) * Driver::HEIGHT) / 8;
  202. auto *fillbuf = new uint8_t[plane_bytes];
  203. PlaneInvert r;
  204. // Drive the SAME call path (writeImage) that produced p0cmd/p1cmd, not
  205. // writeScreenBuffer: some drivers (e.g. GxEPD2_270, "current" buffer 0x13
  206. // vs image-plane 0x15) use a different command for the screen-buffer clear
  207. // than for writeImage's own plane write. Probing via writeScreenBuffer then
  208. // searches the trace for a command (p0cmd) that call never emits, silently
  209. // returning "not found" (-1) and misreporting a true 1bpp panel as packed.
  210. auto cap0 = [&](uint8_t value) -> int {
  211. memset(fillbuf, value, plane_bytes);
  212. Trace::start_event_record();
  213. d.writeImage(fillbuf, fillbuf, 0, 0, Driver::WIDTH, Driver::HEIGHT);
  214. Trace::stop_event_record();
  215. return burst_byte_after(group(0, Trace::events.size()), p0cmd);
  216. };
  217. r.w0_white = cap0(0xFF);
  218. delete[] fillbuf;
  219. r.is_1bpp = (r.w0_white == 0x00 || r.w0_white == 0xFF);
  220. r.inv0 = (r.w0_white == 0x00); // engine plane0 bit=white; white wire 0x00 => invert
  221. if (plane_count > 1) {
  222. if constexpr (has_wsb2<Driver>::value) {
  223. Trace::start_event_record();
  224. d.writeScreenBuffer((uint8_t) 0xFF, (uint8_t) 0x00); // all-red fill
  225. Trace::stop_event_record();
  226. r.w1_red = burst_byte_after(group(0, Trace::events.size()), p1cmd);
  227. r.plane1_valid = (r.w1_red == 0x00 || r.w1_red == 0xFF);
  228. r.inv1 = (r.w1_red == 0x00); // engine plane1 bit=red; red wire 0x00 => invert
  229. }
  230. }
  231. return r;
  232. }
  233. // Generic packed resolver for a driver with NO colour wrapper to drive (unlike
  234. // emit_palette_4c.h/7c.h, which observe a GxEPD2_4C/7C wrapper's named-colour
  235. // fills) - the gap GxEPD2_583 (UC8159c) exposed: it's wrapped GxEPD2_BW
  236. // (binary), but `_send8pixel` expands each 1bpp buffer byte into a wider wire
  237. // format using only two of its codes. Drives the plain Driver's own
  238. // writeImage(black,color,...) - the SAME call the main capture already makes
  239. // - with a uniform all-white (0xFF) and all-black (0x00) 1bpp buffer. If both
  240. // wire bursts are internally uniform (one repeated byte - true for a per-bit
  241. // code lookup with no cross-pixel dependency) and the byte count divides the
  242. // pixel count evenly, that ratio directly gives bits_per_pixel; the MSB field
  243. // of each uniform byte gives that colour's code. A driver whose packing
  244. // depends on more than one bit at a time (dithering, error diffusion) fails
  245. // the uniformity check and stays an honest TODO rather than a wrong guess.
  246. template<typename Driver> PackedResolverFn make_resolver_bw_packed(uint8_t p0cmd) {
  247. return [p0cmd](uint8_t /*clear_fill*/, const PlaneInvert & /*pv*/) -> PackedResolved {
  248. PackedResolved r;
  249. auto fail = [&](const char *why) {
  250. r.resolved = false;
  251. r.note = why;
  252. return r;
  253. };
  254. constexpr int16_t CS = 10, DC = 11, RST = 12, BUSY = 13;
  255. Trace::set_dc_pin(DC);
  256. Peek<Driver> d(CS, DC, RST, BUSY);
  257. Trace::last_busy_release_value = d.busy_level() == HIGH ? LOW : HIGH;
  258. d.selectSPI(SPI, SPISettings());
  259. d.init(0);
  260. const uint16_t W = Driver::WIDTH, H = Driver::HEIGHT;
  261. const uint32_t plane_bytes = (uint32_t) W * H / 8; // 1bpp-sized source buffer
  262. auto *buf = new uint8_t[plane_bytes];
  263. auto burst_for = [&](uint8_t value) -> std::vector<uint8_t> {
  264. memset(buf, value, plane_bytes);
  265. Trace::start_event_record();
  266. d.writeImage(buf, buf, 0, 0, W, H);
  267. Trace::stop_event_record();
  268. return burst_data_after(group(0, Trace::events.size()), p0cmd);
  269. };
  270. const std::vector<uint8_t> white = burst_for(0xFF); // engine convention: bit=1 => white
  271. const std::vector<uint8_t> black = burst_for(0x00);
  272. delete[] buf;
  273. if (white.empty() || black.empty())
  274. return fail("bw-packed probe: no plane-write burst found");
  275. if (white.size() != black.size())
  276. return fail("bw-packed probe: white/black burst lengths differ");
  277. const uint32_t total_pixels = (uint32_t) W * H;
  278. if (white.size() == 0 || total_pixels % white.size() != 0)
  279. return fail("bw-packed probe: pixel count not a multiple of the wire-byte count");
  280. const int ppb = (int) (total_pixels / white.size()); // pixels per wire byte
  281. if (ppb <= 0 || ppb > 8 || (8 % ppb) != 0)
  282. return fail("bw-packed probe: implied pixels/byte doesn't divide 8 evenly");
  283. const int bpp = 8 / ppb;
  284. const uint8_t wbyte = white[0], bbyte = black[0];
  285. for (uint8_t v : white)
  286. if (v != wbyte)
  287. return fail("bw-packed probe: white-fill wire byte not uniform (packing depends on more than one bit)");
  288. for (uint8_t v : black)
  289. if (v != bbyte)
  290. return fail("bw-packed probe: black-fill wire byte not uniform (packing depends on more than one bit)");
  291. if (wbyte == bbyte)
  292. return fail("bw-packed probe: white and black fills produced the same wire byte");
  293. const uint8_t mask = (uint8_t) ((1u << bpp) - 1);
  294. const uint8_t white_code = (wbyte >> (bpp * (ppb - 1))) & mask; // MSB field (every field equal, uniform byte)
  295. const uint8_t black_code = (bbyte >> (bpp * (ppb - 1))) & mask;
  296. r.resolved = true;
  297. r.bits_per_pixel = bpp;
  298. r.color_count = 2;
  299. r.two_code_bw = true;
  300. r.blank_code = white_code;
  301. r.black_code = black_code;
  302. char note[96];
  303. snprintf(note, sizeof(note), "derived from raw 1bpp fills, no colour wrapper: black=0x%02X white=0x%02X, %d px/wire-byte",
  304. black_code, white_code, ppb);
  305. r.note = note;
  306. return r;
  307. };
  308. }
  309. // Plane-sized (>= PLANE_BURST_MIN) burst commands one call makes, from a
  310. // fresh instance with an untraced writeScreenBuffer() first (same trick
  311. // probe_plane_invert_values uses) to absorb the lazy _InitDisplay()/
  312. // _initial_write clear. Shared by probe_full_refresh_extra_write_cmd below so
  313. // it can tell a genuinely NEW burst from one the plain single-bitmap
  314. // writeImage() already makes on its own - a 2-plane (3-colour) driver's
  315. // single-bitmap writeImage(bitmap,...) forwards to writeImage(black, NULL,
  316. // ...), which bursts to BOTH its real plane_write_cmd[0] (real data) AND
  317. // plane_write_cmd[1] (a NULL-color blank-fill burst, same size) every time,
  318. // with no full-refresh involved at all - diffing against just plane_write_cmd
  319. // [0] would misread that second plane's ordinary burst as a full-refresh-only
  320. // extra write.
  321. template<typename Driver, typename Call> std::set<uint8_t> plane_burst_cmds(Call call) {
  322. constexpr int16_t CS = 10, DC = 11, RST = 12, BUSY = 13;
  323. Trace::set_dc_pin(DC);
  324. Peek<Driver> d(CS, DC, RST, BUSY);
  325. Trace::last_busy_release_value = d.busy_level() == HIGH ? LOW : HIGH;
  326. d.selectSPI(SPI, SPISettings());
  327. d.init(0);
  328. d.writeScreenBuffer(); // untraced: absorb lazy init + the _initial_write clear
  329. const uint32_t plane_bytes = (uint32_t) Driver::WIDTH * Driver::HEIGHT / 8;
  330. auto *buf = new uint8_t[plane_bytes];
  331. memset(buf, 0xA5, plane_bytes);
  332. Trace::start_event_record();
  333. call(d, buf);
  334. Trace::stop_event_record();
  335. delete[] buf;
  336. std::set<uint8_t> cmds;
  337. for (const auto &c : group(0, Trace::events.size()))
  338. if (c.data.size() >= PLANE_BURST_MIN)
  339. cmds.insert(c.cmd);
  340. return cmds;
  341. }
  342. // Detects a controller whose writeImageForFullRefresh() diverges from plain
  343. // writeImage() - e.g. SSD1680/SSD1619-family controllers write the same
  344. // bitmap to a "previous RAM" bank (a different command) before the normal
  345. // "current RAM" write plane_write_cmd[plane] already captures. GxEPD2_EPD's
  346. // base class defaults writeImageForFullRefresh() to call writeImage()
  347. // unchanged, so this probe generalizes across every controller without
  348. // hardcoding the known-affected families: it diffs the two calls' burst-
  349. // command sets and returns whatever writeImageForFullRefresh() adds. Returns
  350. // 0x00 (no divergence - by far the common case) or the extra command byte.
  351. template<typename Driver> uint8_t probe_full_refresh_extra_write_cmd() {
  352. auto write_cmds = plane_burst_cmds<Driver>(
  353. [](Peek<Driver> &d, uint8_t *buf) { d.writeImage(buf, 0, 0, Driver::WIDTH, Driver::HEIGHT); });
  354. auto full_cmds = plane_burst_cmds<Driver>(
  355. [](Peek<Driver> &d, uint8_t *buf) { d.writeImageForFullRefresh(buf, 0, 0, Driver::WIDTH, Driver::HEIGHT); });
  356. for (uint8_t c : full_cmds)
  357. if (!write_cmds.count(c))
  358. return c;
  359. return 0x00;
  360. }
  361. // Prints just the plane_write_cmd_for_full_refresh{} line (for splicing into
  362. // an existing hand-maintained panel_*.h, same workflow as
  363. // print_power_sequences / tools/capture.py --power). Self-contained like
  364. // print_power_sequences: derives p0cmd itself (the first plane-sized burst a
  365. // plain writeImage() produces) rather than trusting a shipped panel_*.h's
  366. // plane_write_cmd, which may have been hand-adjusted since it was captured.
  367. template<typename Driver> void print_full_refresh_extra_write() {
  368. constexpr int16_t CS = 10, DC = 11, RST = 12, BUSY = 13;
  369. Trace::set_dc_pin(DC);
  370. Peek<Driver> probe(CS, DC, RST, BUSY);
  371. Trace::last_busy_release_value = probe.busy_level() == HIGH ? LOW : HIGH;
  372. probe.selectSPI(SPI, SPISettings());
  373. probe.init(0);
  374. probe.writeScreenBuffer(); // untraced: absorb lazy init + the _initial_write clear
  375. const uint32_t plane_bytes = (uint32_t) Driver::WIDTH * Driver::HEIGHT / 8;
  376. auto *buf = new uint8_t[plane_bytes];
  377. memset(buf, 0xFF, plane_bytes); // 0xFF/0x00, not a filler byte: doubles as the 1bpp-vs-packed check below
  378. Trace::start_event_record();
  379. probe.writeImage(buf, 0, 0, Driver::WIDTH, Driver::HEIGHT);
  380. Trace::stop_event_record();
  381. delete[] buf;
  382. uint8_t p0cmd = 0x00;
  383. int w0_white = -1;
  384. for (const auto &c : group(0, Trace::events.size()))
  385. if (c.data.size() >= PLANE_BURST_MIN) {
  386. p0cmd = c.cmd;
  387. w0_white = c.data[0];
  388. break;
  389. }
  390. // A packed (>1bpp) driver's writeImageForFullRefresh expects its own packed
  391. // bit depth, not the 1bpp-sized buffer above - probing it the same way a
  392. // 1bpp panel is probed can over/under-read and crash (seen on GDEP073E01,
  393. // 4bpp 7-colour). A 1bpp panel's white-fill wire byte is always 0x00 or
  394. // 0xFF (see probe_plane_invert_values); anything else means packed, so bail
  395. // instead of guessing - this field isn't meaningful there anyway (see
  396. // emit_descriptor_impl's pv.is_1bpp gate).
  397. if (w0_white != 0x00 && w0_white != 0xFF) {
  398. printf(" // SKIPPED: packed (>1bpp) panel - writeImageForFullRefresh's dual-write behaviour is "
  399. "specific to the 1bpp plane model, not applicable here\n");
  400. return;
  401. }
  402. const uint8_t extra = probe_full_refresh_extra_write_cmd<Driver>();
  403. if (extra)
  404. printf(" .plane_write_cmd_for_full_refresh = {0x%02X, 0x00}, // writeImageForFullRefresh also "
  405. "re-writes this \"previous RAM\" bank before the 0x%02X write above - captured for future "
  406. "partial-refresh use, not read by this full-refresh-only engine yet\n",
  407. extra, p0cmd);
  408. else
  409. printf(" // writeImageForFullRefresh == writeImage for this controller: no "
  410. ".plane_write_cmd_for_full_refresh entry needed (0x00 default already correct)\n");
  411. }
  412. // Captures powerOff() or hibernate() as its own standalone command sequence -
  413. // NOT folded into the refresh_seq phase-segmentation above, and NOT captured
  414. // from the same live session as each other. Every GxEPD2 driver's powerOff()
  415. // guards on `_power_is_on` (set true by the refresh() a normal update ends
  416. // with), and hibernate() calls powerOff() again internally - so capturing
  417. // both from one session would have the second call's powerOff() portion
  418. // silently emit nothing (already off from the first). Each call here gets its
  419. // own fresh instance, replayed through the exact init/clear/write/refresh
  420. // setup the main capture above uses, so `call` always runs with
  421. // _power_is_on true, matching a real update-then-power-down.
  422. template<typename Driver, typename MethodCall> std::vector<Cmd> capture_power_sequence(MethodCall call) {
  423. constexpr int16_t CS = 10, DC = 11, RST = 12, BUSY = 13;
  424. Trace::set_dc_pin(DC);
  425. Peek<Driver> d(CS, DC, RST, BUSY);
  426. Trace::last_busy_release_value = d.busy_level() == HIGH ? LOW : HIGH;
  427. d.selectSPI(SPI, SPISettings());
  428. d.init(0);
  429. const uint32_t plane_bytes = (uint32_t) Driver::WIDTH * Driver::HEIGHT / 8;
  430. auto *buf = new uint8_t[plane_bytes];
  431. memset(buf, 0xA5, plane_bytes);
  432. d.writeScreenBuffer();
  433. d.writeImage(buf, buf, 0, 0, Driver::WIDTH, Driver::HEIGHT);
  434. d.refresh(false);
  435. delete[] buf;
  436. Trace::start_event_record();
  437. call(d);
  438. Trace::stop_event_record();
  439. return group(0, Trace::events.size());
  440. }
  441. // (power_off_sequence, hibernate_sequence) for Driver, each captured via
  442. // capture_power_sequence above. hibernate_sequence is hibernate()'s tail
  443. // PAST whatever it shares with powerOff() - every driver observed so far
  444. // calls powerOff() internally then adds exactly one more command (the
  445. // deep-sleep trigger) - since power_off_sequence already has that shared
  446. // part and CycleEngine runs the two back to back (see epd_command.h); a
  447. // prefix mismatch (seen nowhere so far) falls back to the full hibernate
  448. // capture rather than guess.
  449. template<typename Driver> std::pair<std::vector<Cmd>, std::vector<Cmd>> capture_power_off_and_hibernate() {
  450. std::vector<Cmd> power_off_seq = capture_power_sequence<Driver>([](Peek<Driver> &d) { d.powerOff(); });
  451. std::vector<Cmd> hibernate_full_seq = capture_power_sequence<Driver>([](Peek<Driver> &d) { d.hibernate(); });
  452. std::vector<Cmd> hibernate_seq;
  453. if (hibernate_full_seq.size() > power_off_seq.size()) {
  454. bool prefix_matches = true;
  455. for (size_t i = 0; i < power_off_seq.size() && prefix_matches; i++)
  456. if (hibernate_full_seq[i].cmd != power_off_seq[i].cmd || hibernate_full_seq[i].data != power_off_seq[i].data)
  457. prefix_matches = false;
  458. if (prefix_matches)
  459. hibernate_seq.assign(hibernate_full_seq.begin() + power_off_seq.size(), hibernate_full_seq.end());
  460. }
  461. if (hibernate_seq.empty())
  462. hibernate_seq = hibernate_full_seq;
  463. return {power_off_seq, hibernate_seq};
  464. }
  465. // Prints just the POWER_OFF/HIBERNATE arrays + EpdCommand tables (plus the
  466. // descriptor-field lines to paste) for Driver - for splicing
  467. // power_off_sequence/hibernate_sequence into an EXISTING, hand-maintained
  468. // panel_*.h (one shipped before these fields existed) without regenerating,
  469. // and losing, everything else already in it. tools/capture.py --power.
  470. template<typename Driver> void print_power_sequences(const char *macro) {
  471. auto captured = capture_power_off_and_hibernate<Driver>();
  472. const std::vector<Cmd> &power_off_seq = captured.first;
  473. const std::vector<Cmd> &hibernate_seq = captured.second;
  474. auto emit_seq = [&](const char *seqsuffix, const std::vector<Cmd> &cmds) {
  475. for (size_t i = 0; i < cmds.size(); i++)
  476. if (!cmds[i].data.empty()) {
  477. char suf[64];
  478. snprintf(suf, sizeof(suf), "%s_%zu_%02X", seqsuffix, i, cmds[i].cmd);
  479. print_data_array(macro, suf, cmds[i].data);
  480. }
  481. printf("\ninline const EpdCommand %s_%s[] = {\n", macro, seqsuffix);
  482. for (size_t i = 0; i < cmds.size(); i++) {
  483. char suf[64];
  484. snprintf(suf, sizeof(suf), "%s_%zu_%02X", seqsuffix, i, cmds[i].cmd);
  485. print_cmd(macro, suf, cmds[i]);
  486. }
  487. printf("};\n");
  488. };
  489. if (!power_off_seq.empty()) {
  490. emit_seq("POWER_OFF", power_off_seq);
  491. printf("// splice into the descriptor:\n");
  492. printf("// .power_off_sequence = %s_POWER_OFF,\n", macro);
  493. printf("// .power_off_sequence_len = sizeof(%s_POWER_OFF) / sizeof(%s_POWER_OFF[0]),\n", macro, macro);
  494. } else {
  495. printf("// powerOff(): empty capture - this driver's full refresh already leaves\n"
  496. "// _power_is_on false (its _Update_Full() powers itself down), so powerOff()\n"
  497. "// has nothing left to do. No power_off_sequence to add.\n");
  498. }
  499. printf("\n");
  500. if (!hibernate_seq.empty()) {
  501. emit_seq("HIBERNATE", hibernate_seq);
  502. printf("// splice into the descriptor:\n");
  503. printf("// .hibernate_sequence = %s_HIBERNATE,\n", macro);
  504. printf("// .hibernate_sequence_len = sizeof(%s_HIBERNATE) / sizeof(%s_HIBERNATE[0]),\n", macro, macro);
  505. } else {
  506. printf("// hibernate(): empty capture (unexpected - every driver seen so far adds at\n"
  507. "// least the deep-sleep trigger when rst is wired). Review by hand.\n");
  508. }
  509. }
  510. template<typename Driver>
  511. void emit_descriptor_impl(const char *class_name, const char *macro, const PackedResolverFn &resolve_packed) {
  512. constexpr int16_t CS = 10, DC = 11, RST = 12, BUSY = 13;
  513. Trace::set_dc_pin(DC);
  514. Peek<Driver> driver(CS, DC, RST, BUSY);
  515. const int bl = driver.busy_level();
  516. Trace::last_busy_release_value = bl == HIGH ? LOW : HIGH;
  517. driver.selectSPI(SPI, SPISettings());
  518. driver.init(0);
  519. const uint32_t plane_bytes = (static_cast<uint32_t>(Driver::WIDTH) * Driver::HEIGHT) / 8;
  520. auto *black = new uint8_t[plane_bytes];
  521. auto *color = new uint8_t[plane_bytes];
  522. for (uint32_t i = 0; i < plane_bytes; i++) {
  523. black[i] = 0xA5;
  524. color[i] = 0xA5;
  525. }
  526. Trace::start_event_record();
  527. const size_t s0 = 0;
  528. driver.writeScreenBuffer(); // init + clear burst
  529. const size_t s1 = Trace::events.size();
  530. driver.writeImage(black, color, 0, 0, Driver::WIDTH, Driver::HEIGHT); // window + plane
  531. const size_t s2 = Trace::events.size();
  532. driver.refresh(false); // window-again + trigger
  533. const size_t s3 = Trace::events.size();
  534. Trace::stop_event_record();
  535. delete[] black;
  536. delete[] color;
  537. // --- segment ---
  538. auto init_stage = group(s0, s1);
  539. auto image_stage = group(s1, s2);
  540. auto refresh_stage = group(s2, s3);
  541. auto cmds_equal = [](const std::vector<Cmd> &a, const std::vector<Cmd> &b) {
  542. if (a.size() != b.size())
  543. return false;
  544. for (size_t i = 0; i < a.size(); i++)
  545. if (a[i].cmd != b[i].cmd || a[i].data != b[i].data)
  546. return false;
  547. return true;
  548. };
  549. // Every pixel-plane burst in the image call. One => single-plane panel;
  550. // N => N planes (two colour planes sharing a region, or two physical
  551. // regions - told apart below by whether their window-sets match).
  552. std::vector<size_t> bursts;
  553. for (size_t i = 0; i < image_stage.size(); i++)
  554. if (image_stage[i].data.size() >= PLANE_BURST_MIN)
  555. bursts.push_back(i);
  556. const size_t nplanes = bursts.empty() ? 1 : bursts.size();
  557. if (getenv("EMIT_DEBUG")) {
  558. fprintf(stderr, "image_stage %zu cmds; bursts:", image_stage.size());
  559. for (auto b : bursts)
  560. fprintf(stderr, " [%zu]=0x%02X", b, image_stage[b].cmd);
  561. fprintf(stderr, "\n");
  562. for (size_t i = 0; i < image_stage.size(); i++)
  563. fprintf(stderr, " [%zu] 0x%02X data=%zu\n", i, image_stage[i].cmd, image_stage[i].data.size());
  564. }
  565. // pre_block(k) = commands after burst k-1 (or the start) up to, but not
  566. // including, burst k's own plane-write command - i.e. the window set for
  567. // plane k, possibly with the lazily-run _InitDisplay in front for k==0.
  568. auto pre_block = [&](size_t k) {
  569. size_t lo = (k == 0) ? 0 : bursts[k - 1] + 1;
  570. return std::vector<Cmd>(image_stage.begin() + lo, image_stage.begin() + bursts[k]);
  571. };
  572. std::vector<Cmd> init_seq, window0, window1;
  573. std::vector<uint8_t> plane_cmds;
  574. bool per_plane = false;
  575. bool dual_region = false; // two physical regions (per-region select + window)
  576. int plane_width_val = 0; // decoded per-region width (dual_region only)
  577. uint32_t region0_burst = 0; // region 0's image burst length, for line-order detection
  578. if (bursts.size() >= 2) {
  579. // Multi-plane. Two observed layouts:
  580. // interleaved: [init][W0][burst0][W1][burst1] - each plane's window
  581. // sits between its burst and the previous one (dual region);
  582. // batched: [init][W][W][burst0][burst1] - all windows precede all
  583. // bursts (SSD1680 3-colour: writeImage sets the black and
  584. // red RAM windows, then bursts both planes).
  585. for (size_t k = 0; k < bursts.size(); k++)
  586. plane_cmds.push_back(image_stage[bursts[k]].cmd);
  587. const size_t n = bursts.size();
  588. std::vector<Cmd> pre = pre_block(0);
  589. std::vector<std::vector<Cmd>> windows;
  590. bool interleaved = false;
  591. for (size_t k = 1; k < n; k++)
  592. if (bursts[k] - bursts[k - 1] > 1) {
  593. interleaved = true;
  594. break;
  595. }
  596. if (interleaved) {
  597. std::vector<Cmd> gap0(image_stage.begin() + bursts[0] + 1, image_stage.begin() + bursts[1]);
  598. const size_t wl = gap0.size();
  599. if (wl <= pre.size()) {
  600. windows.push_back(std::vector<Cmd>(pre.end() - wl, pre.end()));
  601. init_seq.assign(pre.begin(), pre.end() - wl);
  602. } else {
  603. windows.push_back({});
  604. init_seq = pre;
  605. }
  606. for (size_t k = 1; k < n; k++)
  607. windows.push_back(std::vector<Cmd>(image_stage.begin() + bursts[k - 1] + 1, image_stage.begin() + bursts[k]));
  608. } else {
  609. // Batched: pre = init + n copies of the window. Peel the smallest block
  610. // that repeats n times at the tail; the remainder is init.
  611. size_t L = 0;
  612. for (size_t cand = 1; cand * n <= pre.size(); cand++) {
  613. bool ok = true;
  614. for (size_t b = 0; b < n && ok; b++)
  615. for (size_t j = 0; j < cand && ok; j++) {
  616. const Cmd &a = pre[pre.size() - cand * n + b * cand + j];
  617. const Cmd &c = pre[pre.size() - cand + j];
  618. if (a.cmd != c.cmd || a.data != c.data)
  619. ok = false;
  620. }
  621. if (ok) {
  622. L = cand;
  623. break;
  624. }
  625. }
  626. if (L > 0) {
  627. for (size_t b = 0; b < n; b++)
  628. windows.push_back(std::vector<Cmd>(pre.end() - L * (n - b), pre.end() - L * (n - b - 1)));
  629. init_seq.assign(pre.begin(), pre.end() - L * n);
  630. } else {
  631. windows.push_back({});
  632. init_seq = pre;
  633. }
  634. }
  635. window0 = windows.empty() ? std::vector<Cmd>{} : windows[0];
  636. window1 = windows.size() > 1 ? windows[1] : std::vector<Cmd>{};
  637. for (size_t k = 1; k < windows.size(); k++)
  638. if (!cmds_equal(windows[0], windows[k]))
  639. per_plane = true; // per-plane windows differ => separate regions
  640. // Dual-region refinement (e.g. GDEY0579F51 / HX8717: a two-chip panel whose
  641. // "planes" are two physical half-width regions, each gated by a region-
  642. // select command before its own window + burst). The generic interleaved
  643. // path above already recovers the per-region windows and plane commands;
  644. // three things it can't are filled here.
  645. if (interleaved && per_plane) {
  646. dual_region = true;
  647. std::set<uint8_t> wops; // region-select + window-set opcodes
  648. for (const auto &c : window0)
  649. wops.insert(c.cmd);
  650. for (const auto &c : window1)
  651. wops.insert(c.cmd);
  652. // (1) INIT. A dual-region panel runs its whole _InitDisplay during the
  653. // earlier writeScreenBuffer clear, and that init REUSES the region-select
  654. // opcode (select master/slave for each region's PSR), so the single-plane
  655. // "cut at the first window opcode" rule would cut at the very first init
  656. // command. Instead take init from the clear stage and drop only the
  657. // CONTIGUOUS trailing run of window-set commands before the first clear
  658. // burst (the both-/first-region window-sets); the select/PSR pairs earlier
  659. // in init survive because a non-window command (BTST, power-on, ...)
  660. // separates them from that trailing run.
  661. const size_t cb = plane_index(init_stage); // first clear burst
  662. size_t cut = cb;
  663. while (cut > 0 && wops.count(init_stage[cut - 1].cmd))
  664. cut--;
  665. init_seq.assign(init_stage.begin(), init_stage.begin() + cut);
  666. // (2) plane_width: each region addresses its own half. Decode it from the
  667. // window-set command's payload - first two big-endian 16-bit fields are
  668. // x and x_end, as every window encoder observed so far lays them out.
  669. for (const auto &c : window0)
  670. if (c.data.size() >= 4) {
  671. int x = (c.data[0] << 8) | c.data[1];
  672. int xe = (c.data[2] << 8) | c.data[3];
  673. plane_width_val = xe - x + 1;
  674. break;
  675. }
  676. // (3) line doubling: region 0's burst length, compared in the emit stage
  677. // against plane_width*height*bpp/8 to flag PAIRED_EDGES.
  678. region0_burst = (uint32_t) image_stage[bursts[0]].data.size();
  679. }
  680. } else {
  681. // Single plane: window = the image call's pre-burst block; init comes
  682. // from the writeScreenBuffer stage, cut at the first window-set command
  683. // (SSD16xx sets the window there too - that belongs in window_sequence).
  684. const size_t wp = plane_index(image_stage);
  685. window0.assign(image_stage.begin(), image_stage.begin() + wp);
  686. std::set<uint8_t> wops;
  687. for (const auto &c : window0)
  688. wops.insert(c.cmd);
  689. const size_t ip = plane_index(init_stage);
  690. size_t cut = ip;
  691. for (size_t i = 0; i < ip; i++)
  692. if (wops.count(init_stage[i].cmd)) {
  693. cut = i;
  694. break;
  695. }
  696. init_seq.assign(init_stage.begin(), init_stage.begin() + cut);
  697. plane_cmds.push_back(wp < image_stage.size() ? image_stage[wp].cmd : 0x00);
  698. }
  699. // blank/clear fill byte, observed from the writeScreenBuffer clear burst.
  700. const size_t cip = plane_index(init_stage);
  701. const uint8_t clear_fill = (cip < init_stage.size() && !init_stage[cip].data.empty()) ? init_stage[cip].data[0] : 0x00;
  702. // plane_invert + 1bpp/packed detection, derived by the fill-capture probe.
  703. const uint8_t p0cmd = plane_cmds.empty() ? 0x00 : plane_cmds[0];
  704. const uint8_t p1cmd = plane_cmds.size() > 1 ? plane_cmds[1] : 0x00;
  705. const PlaneInvert pv = probe_plane_invert_values<Driver>(p0cmd, p1cmd, nplanes);
  706. // Packed-family resolution (4-colour palette etc.), run once here so both the
  707. // busy-timeout class and the packed fields below use the same answer. 1bpp
  708. // panels skip it. A specific colour-wrapper resolver (4-colour/7-colour,
  709. // passed in by emit_descriptor_4c/_7c) takes priority; absent one (the
  710. // default emit_descriptor<Driver> entry - a plain, possibly still-mono-
  711. // wrapped driver), fall back to the generic no-wrapper probe, which catches
  712. // a GxEPD2_BW driver that packs internally (see make_resolver_bw_packed).
  713. // An unresolved packed panel (neither resolver recognised it) is treated as
  714. // the slow class for the timeout floor.
  715. const PackedResolverFn effective_resolver = resolve_packed ? resolve_packed : make_resolver_bw_packed<Driver>(p0cmd);
  716. const PackedResolved pr = (!pv.is_1bpp && effective_resolver) ? effective_resolver(clear_fill, pv) : PackedResolved{};
  717. const int depth_bpp = pv.is_1bpp ? 1 : (pr.resolved ? pr.bits_per_pixel : 4);
  718. // Refresh = post-write commands after the LAST burst (e.g. UC8179's 0x92),
  719. // then the refresh call minus any window-set it repeats.
  720. std::set<uint8_t> all_wops;
  721. for (const auto &c : window0)
  722. all_wops.insert(c.cmd);
  723. for (const auto &c : window1)
  724. all_wops.insert(c.cmd);
  725. std::vector<Cmd> refresh_seq;
  726. if (!bursts.empty())
  727. for (size_t i = bursts.back() + 1; i < image_stage.size() && image_stage[i].data.size() < PLANE_BURST_MIN; i++)
  728. refresh_seq.push_back(image_stage[i]);
  729. // A shared-region panel re-sets its one window in refresh() and we drop that
  730. // duplicate; a dual-region panel's refresh instead sets a DISTINCT both-region
  731. // window (its own select payload), which is part of the refresh - keep it.
  732. for (const auto &c : refresh_stage)
  733. if (dual_region || !all_wops.count(c.cmd))
  734. refresh_seq.push_back(c);
  735. // The refresh ends at its trigger (the command we busy-wait on). A dual-region
  736. // driver re-sets the window AFTER the trigger to prime its own next writeImage;
  737. // our engine always sets its own window per cycle, so that trailing window is
  738. // dead in replay - trim anything past the last busy-wait.
  739. if (dual_region) {
  740. size_t last_wait = refresh_seq.size();
  741. for (size_t i = 0; i < refresh_seq.size(); i++)
  742. if (refresh_seq[i].wait_busy)
  743. last_wait = i;
  744. if (last_wait < refresh_seq.size())
  745. refresh_seq.resize(last_wait + 1);
  746. }
  747. // powerOff()/hibernate(), each its own fresh-session capture - see
  748. // capture_power_off_and_hibernate's doc comment.
  749. auto power_captured = capture_power_off_and_hibernate<Driver>();
  750. std::vector<Cmd> &power_off_seq = power_captured.first;
  751. std::vector<Cmd> &hibernate_seq = power_captured.second;
  752. // plane_write_cmd_for_full_refresh - computed here, BEFORE the "// GENERATED"
  753. // marker below, not alongside the printf()s that use it: every probe in this
  754. // function runs its own fresh Peek<Driver> through an un-traced init()/
  755. // writeScreenBuffer() first (see plane_burst_cmds), which prints raw
  756. // pinMode()/digitalWrite()/etc lines straight to stdout outside the
  757. // Trace::event_recording bracket - harmless for every OTHER probe above
  758. // because capture()/run_program() in tools/capture.py slice captured stdout
  759. // from the FIRST "// GENERATED" marker onward, dropping that noise. Calling
  760. // this probe any later - after the marker's printed - would leak its raw
  761. // trace lines into the middle of the emitted descriptor instead.
  762. const uint8_t full_refresh_extra = pv.is_1bpp ? probe_full_refresh_extra_write_cmd<Driver>() : 0x00;
  763. // --- emit ---
  764. printf("// GENERATED by emit_descriptor from %s - candidate.\n", class_name);
  765. printf("// Command sequences, plane layout and plane_invert are observed here;\n");
  766. printf("// for a packed panel bits_per_pixel/palette/blank_code are probe-filled\n");
  767. printf("// when the colour family is recognised (4-colour/7-colour), else left TODO. A\n");
  768. printf("// dual-region panel also gets plane_width and a candidate line_order; the\n");
  769. printf("// exact PAIRED_EDGES row order still wants diag/driver confirmation.\n");
  770. printf("#pragma once\n#include \"epd_command.h\"\n\nnamespace esphome::gxepd2 {\n\n");
  771. if (pr.resolved && pr.two_code_bw) {
  772. printf("// 2-code packed mono (see .bits_per_pixel below): derived purely by observing\n");
  773. printf("// raw white/black fills through the plane-write path, no colour wrapper to\n");
  774. printf("// drive (see make_resolver_bw_packed in emit_descriptor.h).\n");
  775. printf("inline const EpdPaletteEntry %s_PALETTE[] = {\n", macro);
  776. printf(" {0x00, 0x00, 0x00, 0x%02X}, // black\n", pr.black_code & 0xFF);
  777. printf(" {0xFF, 0xFF, 0xFF, 0x%02X}, // white\n", pr.blank_code & 0xFF);
  778. printf("};\n");
  779. printf("inline const uint8_t %s_PALETTE_LEN = sizeof(%s_PALETTE) / sizeof(%s_PALETTE[0]);\n\n", macro, macro, macro);
  780. }
  781. auto emit_seq = [&](const char *seqsuffix, std::vector<Cmd> &cmds) {
  782. for (size_t i = 0; i < cmds.size(); i++)
  783. if (!cmds[i].data.empty()) {
  784. char suf[64];
  785. snprintf(suf, sizeof(suf), "%s_%zu_%02X", seqsuffix, i, cmds[i].cmd);
  786. print_data_array(macro, suf, cmds[i].data);
  787. }
  788. printf("\ninline const EpdCommand %s_%s[] = {\n", macro, seqsuffix);
  789. for (size_t i = 0; i < cmds.size(); i++) {
  790. char suf[64];
  791. snprintf(suf, sizeof(suf), "%s_%zu_%02X", seqsuffix, i, cmds[i].cmd);
  792. print_cmd(macro, suf, cmds[i]);
  793. }
  794. printf("};\n\n");
  795. };
  796. const bool w0_empty = window0.empty(); // e.g. OTP-default panels (F52) set no window
  797. emit_seq("INIT", init_seq);
  798. if (!w0_empty)
  799. emit_seq("WINDOW_SEQ0", window0);
  800. if (per_plane)
  801. emit_seq("WINDOW_SEQ1", window1);
  802. emit_seq("REFRESH", refresh_seq);
  803. const bool has_power_off = !power_off_seq.empty();
  804. const bool has_hibernate = !hibernate_seq.empty();
  805. if (has_power_off)
  806. emit_seq("POWER_OFF", power_off_seq);
  807. if (has_hibernate)
  808. emit_seq("HIBERNATE", hibernate_seq);
  809. printf("inline const EpdPanelDescriptor %s = {\n", macro);
  810. printf(" .width = %u,\n", (unsigned) Driver::WIDTH);
  811. printf(" .height = %u,\n", (unsigned) Driver::HEIGHT);
  812. printf(" .busy_active_level = %s,\n", bl == HIGH ? "true" : "false");
  813. const char *depth_class = depth_bpp >= 4 ? "4bpp 7-colour" : depth_bpp == 2 ? "2bpp 4-colour" : "1bpp mono/3-colour";
  814. printf(" .busy_timeout_ms = %u, // generous diagnostic ceiling (vendor full_refresh_time %ums, "
  815. "%s class); dev-tunable\n",
  816. (unsigned) default_busy_timeout_ms(Driver::full_refresh_time, depth_bpp),
  817. (unsigned) Driver::full_refresh_time, depth_class);
  818. printf(" .init_sequence = %s_INIT,\n", macro);
  819. printf(" .init_sequence_len = sizeof(%s_INIT) / sizeof(%s_INIT[0]),\n", macro, macro);
  820. if (per_plane) {
  821. printf(" .window_sequence = {%s_WINDOW_SEQ0, %s_WINDOW_SEQ1},\n", macro, macro);
  822. printf(" .window_sequence_len = {sizeof(%s_WINDOW_SEQ0) / sizeof(%s_WINDOW_SEQ0[0]), "
  823. "sizeof(%s_WINDOW_SEQ1) / sizeof(%s_WINDOW_SEQ1[0])},\n",
  824. macro, macro, macro, macro);
  825. printf(" .window_per_plane = true, // per-plane windows differ => separate regions\n");
  826. } else if (w0_empty) {
  827. printf(" .window_sequence = {nullptr, nullptr}, // panel sets no window (OTP-default frame)\n");
  828. printf(" .window_sequence_len = {0, 0},\n");
  829. } else {
  830. printf(" .window_sequence = {%s_WINDOW_SEQ0, nullptr},\n", macro);
  831. printf(" .window_sequence_len = {sizeof(%s_WINDOW_SEQ0) / sizeof(%s_WINDOW_SEQ0[0]), 0},\n", macro, macro);
  832. }
  833. printf(" .plane_count = %zu,\n", nplanes);
  834. printf(" .plane_write_cmd = {0x%02X, 0x%02X},\n", p0cmd, p1cmd);
  835. // full_refresh_extra was computed above, before the "// GENERATED" marker -
  836. // see that computation's comment for why. A packed (>1bpp) driver's
  837. // writeImageForFullRefresh takes the same single-bitmap buffer shape, but
  838. // its _writeImage expects a buffer already packed to its own bit depth -
  839. // probing it with a 1bpp-sized buffer over/under-reads and crashes some
  840. // drivers (e.g. GDEP073E01, 4bpp 7-colour), hence the pv.is_1bpp gate above;
  841. // packed panels just keep the {0,0} default.
  842. if (full_refresh_extra)
  843. printf(" .plane_write_cmd_for_full_refresh = {0x%02X, 0x00}, // writeImageForFullRefresh also "
  844. "re-writes this \"previous RAM\" bank before the 0x%02X write above - not read by this "
  845. "full-refresh-only engine yet, captured for future partial-refresh use\n",
  846. full_refresh_extra, p0cmd);
  847. // Fields below are emitted in struct-declaration order (plane_invert,
  848. // plane_width, line_order, bits_per_pixel, palette, palette_len,
  849. // blank_code) so the designated initializer stays well-formed.
  850. if (pv.is_1bpp) {
  851. const bool i1 = (nplanes > 1 && pv.plane1_valid) ? pv.inv1 : false;
  852. printf(" .plane_invert = {%s, %s}, // probe-derived\n", pv.inv0 ? "true" : "false", i1 ? "true" : "false");
  853. } else {
  854. printf(" .plane_invert = {false, false}, // unused: packed panel\n");
  855. }
  856. if (per_plane) {
  857. if (dual_region && plane_width_val > 0)
  858. printf(" .plane_width = %d, // decoded from the region window-set (x_end-x+1); sanity-check\n",
  859. plane_width_val);
  860. else
  861. printf(" .plane_width = 0, // TODO: diag pass (separate-region width)\n");
  862. }
  863. // line_order: a dual-region panel whose per-region burst is twice the region's
  864. // own pixel bytes is sending two wire lines per row (GxEPD2 writeNative's
  865. // doubled, both-edges-to-middle emission). The doubling is observable here;
  866. // the exact row order that PAIRED_EDGES encodes is NOT (a uniform fill can't
  867. // show which buffer row each line carries), so emit it as a candidate the
  868. // diag native-path pass / driver source confirms.
  869. if (dual_region && plane_width_val > 0 && depth_bpp > 0) {
  870. uint32_t expect = (uint32_t) plane_width_val * Driver::HEIGHT * depth_bpp / 8;
  871. if (expect && region0_burst == 2u * expect)
  872. printf(" .line_order = LineOrder::PAIRED_EDGES, // region burst %u B = 2x %u B (2 lines/row); "
  873. "confirm edges-to-middle order via the diag native-path pass\n",
  874. region0_burst, expect);
  875. else if (expect && region0_burst != expect)
  876. printf(" // NOTE: region burst %u B vs expected %u B (1 line/row) - line order unclear, "
  877. "check with the diag native-path pass\n",
  878. region0_burst, expect);
  879. }
  880. if (pv.is_1bpp) {
  881. // 1bpp plane model: depth confirmed by the probe; palette/blank_code unused.
  882. printf(" .bits_per_pixel = 1, // probe: white fill => all-bits wire byte 0x%02X\n", pv.w0_white & 0xFF);
  883. printf(" .palette = nullptr,\n");
  884. printf(" .palette_len = 0,\n");
  885. } else {
  886. // Packed panel. If a family resolver recognised it (4-colour, via
  887. // emit_palette_4c.h), it derived depth/palette/blank_code by observing the
  888. // driver's own colour fills - emit those (computed once, above). Otherwise
  889. // fall back to a TODO the diag pass fills in by hand (an unknown packed
  890. // family, e.g. a packed panel outside the 4-colour/7-colour resolvers).
  891. if (pr.resolved && pr.two_code_bw) {
  892. printf(" .bits_per_pixel = %d, // probe: %s\n", pr.bits_per_pixel, pr.note.c_str());
  893. printf(" .palette = %s_PALETTE,\n", macro);
  894. printf(" .palette_len = %s_PALETTE_LEN,\n", macro);
  895. printf(" .blank_code = 0x%02X, // observed white-fill code\n", pr.blank_code & 0xFF);
  896. } else if (pr.resolved) {
  897. const int shown_colors = pr.color_count >= 0 ? pr.color_count : (1 << pr.bits_per_pixel);
  898. printf(" .bits_per_pixel = %d, // probe: %d-colour packed%s%s\n", pr.bits_per_pixel,
  899. shown_colors, pr.note.empty() ? "" : " - ", pr.note.c_str());
  900. printf(" .palette = %s,\n", pr.palette_expr);
  901. printf(" .palette_len = %s,\n", pr.palette_len_expr);
  902. printf(" .blank_code = 0x%02X, // observed white fill code\n", pr.blank_code & 0xFF);
  903. } else {
  904. printf(" .bits_per_pixel = 2, // TODO: diag pass - PACKED (white fill wire 0x%02X, not 1bpp)%s%s\n",
  905. pv.w0_white & 0xFF, pr.note.empty() ? "" : " - ", pr.note.c_str());
  906. printf(" .palette = nullptr, // TODO: diag pass\n");
  907. printf(" .palette_len = 0,\n");
  908. printf(" .blank_code = 0x00, // TODO: diag pass (clear fill observed 0x%02X)\n", clear_fill);
  909. }
  910. }
  911. printf(" .refresh_sequence = %s_REFRESH,\n", macro);
  912. printf(" .refresh_sequence_len = sizeof(%s_REFRESH) / sizeof(%s_REFRESH[0]),\n", macro, macro);
  913. if (has_power_off) {
  914. printf(" .power_off_sequence = %s_POWER_OFF, // probe: powerOff(), fresh session after a normal refresh\n", macro);
  915. printf(" .power_off_sequence_len = sizeof(%s_POWER_OFF) / sizeof(%s_POWER_OFF[0]),\n", macro, macro);
  916. }
  917. if (has_hibernate) {
  918. printf(" .hibernate_sequence = %s_HIBERNATE, // probe: hibernate()'s tail past powerOff() (see file header)\n",
  919. macro);
  920. printf(" .hibernate_sequence_len = sizeof(%s_HIBERNATE) / sizeof(%s_HIBERNATE[0]),\n", macro, macro);
  921. }
  922. printf("};\n\n} // namespace esphome::gxepd2\n");
  923. }
  924. // Generic entry: no packed-family resolver, so a packed panel's
  925. // depth/palette/blank_code emit as TODO placeholders (the pre-resolver
  926. // behaviour). The 4-colour entry is emit_descriptor_4c() in emit_palette_4c.h.
  927. template<typename Driver> void emit_descriptor(const char *class_name, const char *macro) {
  928. emit_descriptor_impl<Driver>(class_name, macro, nullptr);
  929. }
  930. } // namespace emit