#pragma once #include namespace esphome::zepd { // One command byte plus its (fixed, compile-time) data bytes, as sent by // GxEPD2_EPD::_writeCommand()/_writeData(). delay_before_ms/delay_after_ms // and wait_busy_after mirror the fixed delay()s and _waitWhileBusy() calls // bracketing specific commands in the vendor source (which command gets // which varies per panel - e.g. GxEPD2_213_Z98c delays 10ms on both sides // of its SWRESET, while GxEPD2_154_Z90c busy-waits after its SWRESET // instead). At most one of delay_after_ms/wait_busy_after is expected to be // set on any one command. struct EpdCommand { uint8_t cmd; const uint8_t *data; // nullptr if the command takes no data bytes uint8_t data_len; uint16_t delay_before_ms{0}; uint16_t delay_after_ms{0}; bool wait_busy_after{false}; }; // One palette entry for a packed (bits_per_pixel > 1) panel: a reference // RGB color (ESPHome Color space, 0-255 per channel) and the wire code the // controller uses for it. Classification at draw time picks the entry // nearest the drawn color, with exact matches winning - so named colors map // deterministically, the way GxEPD2's own color4()/color7() switch does. // Several entries may share a code (GxEPD2 maps green and orange both onto // the 4-color yellow code, and blue onto black). struct EpdPaletteEntry { uint8_t r, g, b; uint8_t code; }; // Standard GxEPD2 4-color (BWRY) palette - codes per GxEPD2_4C.h color4(): // black=0x00, white=0x01, yellow/green/orange=0x02, red=0x03, blue->black. inline const EpdPaletteEntry PALETTE_4C[] = { {0x00, 0x00, 0x00, 0x00}, // black {0xFF, 0xFF, 0xFF, 0x01}, // white {0xFF, 0xFF, 0x00, 0x02}, // yellow {0x00, 0xFF, 0x00, 0x02}, // green -> yellow code {0xFF, 0xA4, 0x00, 0x02}, // orange -> yellow code {0xFF, 0x00, 0x00, 0x03}, // red {0x00, 0x00, 0xFF, 0x00}, // blue -> black code }; inline const uint8_t PALETTE_4C_LEN = sizeof(PALETTE_4C) / sizeof(PALETTE_4C[0]); // Standard ACeP 7-color palette - codes per GxEPD2_7C.h color7(): black=0x00, // white=0x01, green=0x02, blue=0x03, red=0x04, yellow=0x05, orange=0x06. inline const EpdPaletteEntry PALETTE_7C[] = { {0x00, 0x00, 0x00, 0x00}, // black {0xFF, 0xFF, 0xFF, 0x01}, // white {0x00, 0xFF, 0x00, 0x02}, // green {0x00, 0x00, 0xFF, 0x03}, // blue {0xFF, 0x00, 0x00, 0x04}, // red {0xFF, 0xFF, 0x00, 0x05}, // yellow {0xFF, 0xA4, 0x00, 0x06}, // orange }; inline const uint8_t PALETTE_7C_LEN = sizeof(PALETTE_7C) / sizeof(PALETTE_7C[0]); // Everything the engine needs to drive one panel - this is the codegen'd // "engine model" produced from a model's drivers/.json (see // ../../../../claude/DESCRIPTOR_REGISTRY_PLAN.md), hand-derived today from a // GxEPD2 vendor driver's .cpp/.h the same way the legacy // components/gxepd2/panel_*.h files were. Only what the engine actually // uses - the RAM-window-set command layout (0x44/0x45/0x4E/0x4F) is common // to every panel inspected so far and is hardcoded in Engine itself rather // than made part of this struct; revisit if/when a controller family turns // up that lays it out differently. // // Order in which the engine emits a plane's wire lines, derived by the diag // native-path tool. The buffer always holds `height` rows of real pixels; // this controls only which buffer row each wire line carries and how many // wire lines there are, so wire length can exceed buffer size. // SEQUENTIAL - wire line i carries buffer row i; line count == height. // Every single-region panel captured so far. // PAIRED_EDGES - the controller fills a 2x-height RAM scanned from both // edges toward the middle: for i in [0,height) it wants // buffer row (height-1-i) then row i, so line count == // 2*height and each row is sent twice. GDEY0579F51 / HX8717. // This is exactly what GxEPD2's writeNative emits for that // panel (99 bytes/line x 544 lines/region), so a faithful // replay needs it - nothing panel-specific in the engine. enum class LineOrder : uint8_t { SEQUENTIAL, PAIRED_EDGES }; struct EngineModel { uint16_t width; uint16_t height; bool busy_active_level; // digitalRead() level meaning "still busy" uint32_t busy_timeout_ms; // ceiling for every busy wait (init and refresh alike) const EpdCommand *init_sequence; // run once, after the reset pulse uint8_t init_sequence_len; // Sets the (always full-frame, since this engine has no partial-window // support) RAM addressing window. window_sequence[0] runs once after // init_sequence and before the first plane's byte burst; index 1 is only // used when window_per_plane is true (see below). Was hardcoded assuming // every controller shares SSD1680's 0x44/0x45/0x4E/0x4F byte-column // layout - GxEPD2_426_GDEQ0426T82 (SSD1677) turned out to address in raw // pixels with reversed Y and re-issues its entry-mode command here, and // GxEPD2_583_GDEQ0583T31 (UC8179) uses one 0x90 command bracketed by // 0x91/0x92 markers instead. Made per-model data, same as init/refresh, // rather than special-cased in the engine. const EpdCommand *window_sequence[2]; uint8_t window_sequence_len[2]; // true if each plane needs window_sequence[plane index] re-run before // its own byte burst, instead of one window_sequence[0] covering every // plane's burst (the default, e.g. a BW+red panel's shared full-frame // window). This is how a panel with real master/slave regions (two // genuinely separate physical RAM windows, not just two color planes // sharing one) is handled - "plane" here just means "one more buffer // with its own byte burst," which happens to fit a two-physical-region // panel as well as a two-color-plane one. bool window_per_plane{false}; uint8_t plane_count; // 1 (BW), 2 (BW + accent color), or 2 physical regions uint8_t plane_write_cmd[2]; // e.g. {0x24, 0x26} // The extra "previous RAM bank" write GxEPD2's writeImageForFullRefresh() // makes that plain writeImage() doesn't, on controllers where the two // diverge (SSD1680/SSD1681/SSD1619-family: 0x26 then 0x24, or 0x10 then // 0x13 - same bitmap written twice, to two different commands). 0x00 (the // default) means no divergence was observed: writeImageForFullRefresh // falls back to writeImage for that controller, the common case. NOT read // by Engine today - this engine always does a plain single write per // plane, correct for full-refresh-only operation, since the full-refresh // waveform doesn't depend on the "previous" bank's content. It matters // once partial refresh reads that bank to compute its diff, so it's // captured now rather than needing a second capture pass later. uint8_t plane_write_cmd_for_full_refresh[2]{0, 0}; bool plane_invert[2]; // true if the byte must be bit-inverted on write // When window_per_plane is true, each plane's own buffer is sized for // this width, not the descriptor's overall `width` (which is the full // logical canvas the adapter draws into) - e.g. 396 for GDEY0579F51's // master/slave halves, each 396x272 on its own, not the full 792x272. // Unused otherwise. uint16_t plane_width{0}; // Wire-line emission order for this panel's plane(s) - see LineOrder // above. Default SEQUENTIAL; PAIRED_EDGES is not assumed for any panel // without the diag native-path tool deriving it. LineOrder line_order{LineOrder::SEQUENTIAL}; // Pixel bit-depth, which also selects the buffer model: // 1 = one-bit-per-pixel plane model (mono, or BW + accent as two // separate 1bpp planes). plane_invert applies; palette is unused. // 2 = packed 4-color: 2 bits/pixel, 4 pixels/byte, MSB-first. // 4 = packed 7-color (ACeP): 4 bits/pixel, 2 pixels/byte, MSB-first. // For bits_per_pixel > 1 the buffer is a single packed code-per-pixel // stream (per region when window_per_plane), sized width*height*bpp/8, // and each pixel's wire code comes from `palette` (nearest-color // classification at draw time) rather than the 1bpp plane bitmasks; // plane_invert is unused. uint8_t bits_per_pixel{1}; // Packed-mode (bits_per_pixel > 1) palette and the wire code the buffer is // cleared to (the panel's white/blank). Unused when bits_per_pixel == 1. // See PALETTE_4C / PALETTE_7C. const EpdPaletteEntry *palette{nullptr}; uint8_t palette_len{0}; uint8_t blank_code{0}; const EpdCommand *refresh_sequence; // e.g. 0x22/0xF7 then trigger 0x20 uint8_t refresh_sequence_len; // powerOff()/hibernate(), captured the same way as refresh_sequence. // Exposed for a user's own lambda (Engine::request_power_off/ // request_hibernate), not run automatically by update(): this engine does // a full hardware reset on every cycle regardless of prior power state, // so correctness never depends on calling either. hibernate_sequence is // the deep-sleep trigger ONLY (every GxEPD2 driver's hibernate() is // powerOff() + one more command when rst is wired) - Engine runs // power_off_sequence first, then hibernate_sequence, to replay the same // two-step effect. Both nullptr/0 (not yet captured) is a safe no-op: // Engine treats a zero-length sequence as already done. const EpdCommand *power_off_sequence{nullptr}; uint8_t power_off_sequence_len{0}; const EpdCommand *hibernate_sequence{nullptr}; // deep-sleep trigger only, after power_off_sequence uint8_t hibernate_sequence_len{0}; }; } // namespace esphome::zepd