gxepd2_display.h 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. #pragma once
  2. #include "esphome/core/component.h"
  3. #include "esphome/core/helpers.h"
  4. #include "esphome/components/display/display_buffer.h"
  5. #include "esphome/components/spi/spi.h"
  6. #include "cycle_engine.h"
  7. #include "panel_750_t7.h"
  8. #include "panel_750.h"
  9. #include "panel_583_t8.h"
  10. #include "panel_579_gdey0579t93.h"
  11. #include "panel_576_gdeh0576t81_bw_shm.h"
  12. #include "panel_576_gdeh0576t81_bw.h"
  13. #include "panel_420_se0420nq04.h"
  14. #include "panel_420_m01.h"
  15. #include "panel_420_gye042a87.h"
  16. #include "panel_420_gdey042t81.h"
  17. #include "panel_397_gdem0397t81.h"
  18. #include "panel_371.h"
  19. #include "panel_370_tc1.h"
  20. #include "panel_310_gdeq031t10.h"
  21. #include "panel_290_t94_v2.h"
  22. #include "panel_290_t94.h"
  23. #include "panel_290_t5.h"
  24. #include "panel_290_m06.h"
  25. #include "panel_290_i6fd.h"
  26. #include "panel_290_gdey029t94.h"
  27. #include "panel_290_gdey029t71h.h"
  28. #include "panel_290_bs.h"
  29. #include "panel_290.h"
  30. #include "panel_270_gdey027t91.h"
  31. #include "panel_260_m01.h"
  32. #include "panel_260.h"
  33. #include "panel_213_flex.h"
  34. #include "panel_213_t5d.h"
  35. #include "panel_213_m21.h"
  36. #include "panel_213_gdey0213b74.h"
  37. #include "panel_213_bn.h"
  38. #include "panel_213_b74.h"
  39. #include "panel_213_b73.h"
  40. #include "panel_213.h"
  41. #include "panel_154_t8.h"
  42. #include "panel_154_m10.h"
  43. #include "panel_154_m09.h"
  44. #include "panel_154_gdey0154d67.h"
  45. #include "panel_154_d67.h"
  46. #include "panel_150_bn.h"
  47. #include "panel_1330_gdem133t91.h"
  48. #include "panel_1160_t91.h"
  49. #include "panel_102.h"
  50. #include "panel_266_bn.h"
  51. #include "panel_1020_gdem102t91.h"
  52. #include "panel_154.h"
  53. #include "panel_154_z90c.h"
  54. #include "panel_213_b72.h"
  55. #include "panel_213_z98c.h"
  56. #include "panel_213c_gdey0213f51.h"
  57. #include "panel_213c_gdey0213f52.h"
  58. #include "panel_270.h"
  59. #include "panel_290_t5d.h"
  60. #include "panel_290c_gdey029f51.h"
  61. #include "panel_420.h"
  62. #include "panel_426_gdeq0426t82.h"
  63. #include "panel_579c_gdey0579f51.h"
  64. #include "panel_583.h"
  65. #include "panel_583_gdeq0583t31.h"
  66. #include "panel_730c_gdep073e01.h"
  67. #include "panel_750_gdey075t7.h"
  68. namespace esphome::gxepd2 {
  69. static const char *const TAG = "gxepd2";
  70. // ESPHome display component for a GxEPD2 panel. Panels arrive as
  71. // EpdPanelDescriptors selected at runtime via set_descriptor() and driven by
  72. // CycleEngine, not by templating this class over GxEPD2's own driver classes.
  73. class GxEPD2Display : public display::DisplayBuffer {
  74. public:
  75. void set_cs_pin(GPIOPin *pin) { this->cs_ = pin; }
  76. void set_dc_pin(GPIOPin *pin) { this->dc_ = pin; }
  77. void set_reset_pin(GPIOPin *pin) { this->rst_ = pin; }
  78. void set_busy_pin(GPIOPin *pin) { this->busy_ = pin; }
  79. void set_spi_parent(spi::SPIComponent *parent) { this->spi_.set_spi_parent(parent); }
  80. void set_descriptor(const EpdPanelDescriptor *desc) { this->desc_ = desc; }
  81. void set_invert_mono(bool invert) { this->invert_mono_ = invert; }
  82. // The short panel label (e.g. "GDEQ0583T31") test_pattern() can print,
  83. // derived from model: in display.py.
  84. void set_model_name(const char *name) { this->model_name_ = name; }
  85. // Whether this panel's descriptor is confirmed on real hardware (vs captured
  86. // but not yet bench-verified) - surfaced in dump_config(). From display.py.
  87. void set_verified(bool v) { this->verified_ = v; }
  88. // Bench visual-check pattern, bit-depth aware. Everything scales to what
  89. // THIS panel can actually show (see test_colors_): the color-bar count,
  90. // the ladder color cycle and the corner markers all follow the panel's
  91. // real color set instead of a hardcoded 4, so the drawn pattern doubles as
  92. // a statement of what the driver expects of the panel. Adds a dithered grey
  93. // ramp so a 1bpp panel shows a graded sweep, not just two flat blocks.
  94. //
  95. // Pass a font to label the panel with its model (and the panel can spare the
  96. // room); pass nothing to skip the label - handy when you just want a quick
  97. // pattern without defining a font. From the display lambda:
  98. // id(<display_id>).test_pattern(); // no label
  99. // id(<display_id>).test_pattern(id(<font_id>)); // labelled
  100. void test_pattern(display::BaseFont *font = nullptr) {
  101. const int w = this->get_width();
  102. const int h = this->get_height();
  103. Color colors[8];
  104. const int n = this->test_colors_(colors, 8);
  105. this->fill(Color(0xFF, 0xFF, 0xFF));
  106. const int smin = w < h ? w : h;
  107. const int border = smin / 100 > 1 ? smin / 100 : 1;
  108. const int x0 = border;
  109. int y0 = border;
  110. const int iw = w - 2 * border;
  111. int ih = h - 2 * border;
  112. // Name band, above every test zone. Drawn only when a font was passed and
  113. // the panel is tall enough to give it a fifth of the interior without
  114. // crowding the zones below.
  115. if (font != nullptr && this->model_name_ != nullptr && this->model_name_[0] != '\0') {
  116. int tw, xoff, baseline, th;
  117. font->measure(this->model_name_, &tw, &xoff, &baseline, &th);
  118. const int band = th + 4;
  119. if (band <= ih / 5) {
  120. this->print(w / 2, y0 + band / 2, font, Color(0x00, 0x00, 0x00), display::TextAlign::CENTER, this->model_name_);
  121. y0 += band;
  122. ih -= band;
  123. }
  124. }
  125. // Vertical budget for the four zones below any name band.
  126. const int bars_h = ih * 34 / 100;
  127. int dither_h = ih * 12 / 100;
  128. if (dither_h < 8) dither_h = 8;
  129. const int row_h = ih * 18 / 100 > 2 ? ih * 18 / 100 : 2;
  130. const int dy = y0 + bars_h;
  131. const int ry = dy + dither_h;
  132. const int col_y = ry + row_h;
  133. const int col_h = (y0 + ih) - col_y;
  134. // Color bars: one solid block per available color, in palette/wire-code
  135. // order (black, white, then the accents). A wrong or missing block
  136. // points straight at the color - or the wire code - it stands for.
  137. const int bar_w = iw / n;
  138. for (int i = 0; i < n; i++) {
  139. const int bx = x0 + i * bar_w;
  140. const int bw = (i == n - 1) ? (x0 + iw - bx) : bar_w;
  141. this->filled_rectangle(bx, y0, bw, bars_h, colors[i]);
  142. }
  143. // Dithered grey ramp: brightness 255 (left) -> 0 (right) via an ordered
  144. // 4x4 Bayer threshold. On a 1bpp panel this is the only zone that shows
  145. // a graded sweep; on deeper panels it's a quick mono/uniformity check.
  146. static const uint8_t BAYER4[4][4] = {
  147. {0, 8, 2, 10}, {12, 4, 14, 6}, {3, 11, 1, 9}, {15, 7, 13, 5}};
  148. for (int yy = 0; yy < dither_h; yy++) {
  149. for (int xx = 0; xx < iw; xx++) {
  150. const int bright = (iw <= 1) ? 255 : 255 - xx * 255 / (iw - 1);
  151. const int thr = (BAYER4[yy & 3][xx & 3] * 2 + 1) * 255 / 32;
  152. if (bright <= thr)
  153. this->draw_pixel_at(x0 + xx, dy + yy, Color(0x00, 0x00, 0x00));
  154. }
  155. }
  156. // Row ladder: 1px rows cycling the full color set - a dropped, doubled
  157. // or reordered row breaks the N-row cadence.
  158. for (int yy = 0; yy < row_h; yy++)
  159. this->horizontal_line(x0, ry + yy, iw, colors[yy % n]);
  160. // Column ladder: the same rotated 90 degrees, catching nibble/column
  161. // order bugs the row ladder can't (packed formats put several pixels per
  162. // byte, so a column-order fault can leave every row looking fine).
  163. for (int xx = 0; xx < iw; xx++)
  164. this->vertical_line(x0 + xx, col_y, col_h, colors[xx % n]);
  165. // Corner markers for orientation, using the panel's ink colors (every
  166. // color but white). BL is always black; TL/TR take the next inks when
  167. // the panel has them; BR is left blank as the distinct "no marker"
  168. // state. A 90/180 rotation or mirror can't survive this even though the
  169. // bars and ladders are otherwise symmetric.
  170. Color ink[8];
  171. int ni = 0;
  172. for (int i = 0; i < n; i++) {
  173. if (colors[i].red == 0xFF && colors[i].green == 0xFF && colors[i].blue == 0xFF)
  174. continue; // skip white - a white marker is invisible
  175. ink[ni++] = colors[i];
  176. }
  177. const int mk = smin / 12 > 3 ? smin / 12 : 3;
  178. if (ni > 0) this->filled_rectangle(x0, y0 + ih - mk, mk, mk, ink[0]); // BL
  179. if (ni > 1) this->filled_rectangle(x0, y0, mk, mk, ink[1]); // TL
  180. if (ni > 2) this->filled_rectangle(x0 + iw - mk, y0, mk, mk, ink[2]); // TR
  181. // Corner-to-corner diagonals over everything: the cheapest coordinate-
  182. // transform check - a clean line means no row/column reordering.
  183. this->line(0, 0, w - 1, h - 1, Color(0x00, 0x00, 0x00));
  184. this->line(w - 1, 0, 0, h - 1, Color(0x00, 0x00, 0x00));
  185. // Border frame last, so it stays an unbroken rectangle on all four edges
  186. // even where a zone fills to the edge - catches a clipped/offset origin.
  187. this->rectangle(0, 0, w, h, Color(0x00, 0x00, 0x00));
  188. }
  189. void setup() override {
  190. const auto *desc = this->desc_;
  191. // window_per_plane panels (GDEY0579F51's master+slave) size each
  192. // plane's own buffer from plane_width (each region's own local
  193. // width), not the descriptor's overall width (the full logical canvas
  194. // ESPHome draws into) - both regions are the same size here, so one
  195. // plane_bytes_ value covers both buffers.
  196. uint32_t w = desc->window_per_plane ? desc->plane_width : desc->width;
  197. // width*height*bpp/8: /8 at 1bpp, /4 at 2bpp (4-color), /2 at 4bpp (7-color).
  198. this->plane_bytes_ = (uint32_t) w * desc->height * desc->bits_per_pixel / 8;
  199. RAMAllocator<uint8_t> allocator;
  200. this->plane0_ = allocator.allocate(this->plane_bytes_);
  201. bool need_plane1 = desc->plane_count > 1;
  202. this->plane1_ = need_plane1 ? allocator.allocate(this->plane_bytes_) : nullptr;
  203. if (this->plane0_ == nullptr || (need_plane1 && this->plane1_ == nullptr)) {
  204. ESP_LOGE(TAG, "Failed to allocate %u-byte plane buffer(s)", (unsigned) this->plane_bytes_);
  205. this->mark_failed();
  206. return;
  207. }
  208. if (desc->bits_per_pixel > 1) {
  209. // Packed modes: clear every pixel to the panel's white/blank code,
  210. // replicated across the byte - e.g. 4-color white=0x01 -> 0x55, 7-color
  211. // white=0x01 -> 0x11, matching GxEPD2's own writeScreenBuffer() fill
  212. // (color4()*0x55 / color7()|<<4). Both regions of a window_per_plane
  213. // panel (master/slave) get the same blank fill.
  214. uint8_t fill = replicate_code_(desc->blank_code, desc->bits_per_pixel);
  215. memset(this->plane0_, fill, this->plane_bytes_);
  216. if (this->plane1_ != nullptr)
  217. memset(this->plane1_, fill, this->plane_bytes_);
  218. } else {
  219. // 1bpp plane model: plane0 white = all bits set (0xFF); plane1 (the
  220. // accent/red plane, when present) = 0 = not-red.
  221. memset(this->plane0_, 0xFF, this->plane_bytes_);
  222. if (this->plane1_ != nullptr)
  223. memset(this->plane1_, 0x00, this->plane_bytes_);
  224. }
  225. this->spi_.spi_setup();
  226. this->engine_.begin(desc, this->cs_, this->dc_, this->rst_, this->busy_, &this->spi_);
  227. }
  228. // Arms the state machine and returns immediately - the real several-
  229. // seconds-long write/refresh/busy-wait cycle happens across many
  230. // subsequent loop() ticks, not before this call returns.
  231. void update() override {
  232. if (!this->engine_.idle()) {
  233. ESP_LOGW(TAG, "Skipping update: previous display refresh is still running");
  234. return;
  235. }
  236. this->do_update_();
  237. this->engine_.request_full_update(this->plane0_, this->plane1_);
  238. }
  239. void loop() override { this->engine_.step(); }
  240. // True once the engine has returned to idle after the last update()/
  241. // power_off()/hibernate() call. Lets a more complex application (a lambda
  242. // chaining several updates, a deep_sleep automation deciding whether to
  243. // wait) check "is the panel still mid-refresh" instead of guessing from
  244. // timing - the same thing power_off()/hibernate()'s own guards below check
  245. // internally.
  246. bool idle() const { return this->engine_.idle(); }
  247. // Turns off the panel's driving-voltage generation (reduces fading over
  248. // time between refreshes). Call from a lambda, e.g.
  249. // id(<display_id>).power_off(); - not called automatically by update(),
  250. // since this engine does a full hardware reset every cycle regardless of
  251. // prior power state, so correctness never depends on it. (It IS called
  252. // automatically from on_powerdown() below.) Safe to call mid-refresh: see
  253. // CycleEngine::request_power_off().
  254. void power_off() {
  255. if (!this->engine_.idle())
  256. ESP_LOGW(TAG, "power_off() called mid-refresh; aborting the in-progress update");
  257. this->engine_.request_power_off();
  258. }
  259. // power_off() + deep sleep, wakeable only by a hardware reset pulse - which
  260. // the next update() always issues (request_full_update()'s RESET_LOW), so
  261. // no paired "wake" call is needed; just call update() as usual. Safe to
  262. // call mid-refresh: see CycleEngine::request_hibernate().
  263. void hibernate() {
  264. if (!this->engine_.idle())
  265. ESP_LOGW(TAG, "hibernate() called mid-refresh; aborting the in-progress update");
  266. this->engine_.request_hibernate();
  267. }
  268. // ESPHome calls this on every component right before deep_sleep actually
  269. // suspends the MCU, and on safe_reboot() - see
  270. // Application::run_powerdown_hooks() (fires after teardown_components()
  271. // completes or times out, deep_sleep's should_teardown_() is unconditional
  272. // on every supported platform). Powers the panel down with no YAML/lambda
  273. // needed for the common case: hibernate() when a reset pin is wired (the
  274. // next update()'s unconditional RESET_LOW wakes it - see hibernate()
  275. // above), else power_off() (needs no reset pulse to recover, so it's safe
  276. // with no rst pin at all).
  277. //
  278. // Deliberately NOT paired with a teardown() override to wait out a
  279. // mid-flight refresh: ESPHome's teardown budget (1s on reboot, 5s on deep
  280. // sleep) is far shorter than this engine's slower busy_timeout_ms classes
  281. // (up to 45-60s for 4bpp/7-color panels), so stalling teardown here would
  282. // just lose that race every time and get force-advanced anyway - the
  283. // budget is sized for a network flush (see api_server::teardown()), not a
  284. // hardware operation. If a refresh is still running when this fires,
  285. // power_off()/hibernate() abort it with a reset pulse first rather than
  286. // writing a power-down command over a controller that's still asserting
  287. // BUSY and would just ignore it (see CycleEngine::request_power_off()). A
  288. // caller who cares about that race can check idle() (above) and hold off
  289. // triggering deep sleep themselves.
  290. void on_powerdown() override {
  291. if (this->rst_ != nullptr) {
  292. this->hibernate();
  293. } else {
  294. this->power_off();
  295. }
  296. }
  297. #ifdef USE_GXEPD2_DIAGNOSTICS
  298. // Dev-time diagnostics - compiled out entirely unless this display's YAML
  299. // sets `diagnostics: true` (display.py then adds -DUSE_GXEPD2_DIAGNOSTICS),
  300. // so a production build pays nothing for them. A stash for throwaway bench
  301. // tests, not a stable API - add more here as needed, don't feel obliged to
  302. // keep any of it around once a test has done its job.
  303. //
  304. // Draws the current lambda/pages content and pushes it WITHOUT the reset
  305. // pulse update() normally does first - see
  306. // CycleEngine::diag_request_update_no_reset(). Call from a lambda, e.g. to
  307. // check whether power_off()/hibernate() actually took: call one of those,
  308. // then this with a pattern distinct from what's already on screen - no
  309. // change (or odd BUSY behavior) means the panel really is off-line; follow
  310. // with a normal update() to confirm the next real reset wakes it and to
  311. // leave the panel in a known state either way.
  312. void diag_update_no_reset() {
  313. if (!this->engine_.idle()) {
  314. ESP_LOGW(TAG, "Skipping diag_update_no_reset: display refresh still in progress");
  315. return;
  316. }
  317. this->do_update_();
  318. this->engine_.diag_request_update_no_reset(this->plane0_, this->plane1_);
  319. }
  320. #endif
  321. void dump_config() override {
  322. ESP_LOGCONFIG(TAG, "GxEPD2 Display:");
  323. ESP_LOGCONFIG(TAG, " Model: %s", this->model_name_ != nullptr ? this->model_name_ : "(unknown)");
  324. ESP_LOGCONFIG(TAG, " Dimensions: %ux%u", (unsigned) this->desc_->width, (unsigned) this->desc_->height);
  325. ESP_LOGCONFIG(TAG, " Bits/pixel: %u (%s)", (unsigned) this->desc_->bits_per_pixel,
  326. this->desc_->bits_per_pixel == 1 ? "mono/plane" : "packed palette");
  327. ESP_LOGCONFIG(TAG, " Planes/regions: %u", (unsigned) this->desc_->plane_count);
  328. ESP_LOGCONFIG(TAG, " Busy active level: %s", this->desc_->busy_active_level ? "HIGH" : "LOW");
  329. if (this->desc_->bits_per_pixel == 1)
  330. ESP_LOGCONFIG(TAG, " invert_mono: %s", YESNO(this->invert_mono_));
  331. ESP_LOGCONFIG(TAG, " Hardware-verified: %s", YESNO(this->verified_));
  332. if (!this->verified_)
  333. ESP_LOGW(TAG, "Panel %s is not yet hardware-verified - the captured descriptor may be wrong; "
  334. "if it renders correctly, please report it so it can be marked verified",
  335. this->model_name_ != nullptr ? this->model_name_ : "(unknown)");
  336. LOG_UPDATE_INTERVAL(this);
  337. }
  338. display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
  339. protected:
  340. // Replicates a packed wire code across a whole byte (one code per pixel
  341. // slot), e.g. 2bpp code 0x01 -> 0x55, 4bpp code 0x01 -> 0x11.
  342. static uint8_t replicate_code_(uint8_t code, uint8_t bpp) {
  343. uint8_t mask = static_cast<uint8_t>((1u << bpp) - 1);
  344. uint8_t out = 0;
  345. for (uint8_t i = 0; i < 8 / bpp; i++)
  346. out = static_cast<uint8_t>((out << bpp) | (code & mask));
  347. return out;
  348. }
  349. // Maps a drawn RGB color to its panel wire code via the descriptor's
  350. // palette: exact matches win (so named colors are deterministic, like
  351. // GxEPD2's color4()/color7() switch), otherwise nearest in RGB space.
  352. static uint8_t classify_(Color c, const EpdPaletteEntry *pal, uint8_t len) {
  353. if (pal == nullptr || len == 0)
  354. return 0;
  355. uint8_t best = 0;
  356. long best_d = -1;
  357. for (uint8_t i = 0; i < len; i++) {
  358. long dr = static_cast<long>(c.red) - pal[i].r;
  359. long dg = static_cast<long>(c.green) - pal[i].g;
  360. long db = static_cast<long>(c.blue) - pal[i].b;
  361. long d = dr * dr + dg * dg + db * db;
  362. if (d == 0)
  363. return pal[i].code;
  364. if (best_d < 0 || d < best_d) {
  365. best_d = d;
  366. best = i;
  367. }
  368. }
  369. return pal[best].code;
  370. }
  371. // Packed code-per-pixel model (bits_per_pixel > 1): N bits/pixel, MSB
  372. // first, matching GxEPD2's own out_data packing in writeImage (4-color
  373. // 4px/byte, 7-color 2px/byte). Routes to the correct region for a
  374. // window_per_plane (master/slave) panel the same way the mono path does.
  375. void draw_packed_(int x, int y, Color color) {
  376. const uint8_t bpp = this->desc_->bits_per_pixel;
  377. uint8_t code = classify_(color, this->desc_->palette, this->desc_->palette_len);
  378. uint16_t stride = this->desc_->width;
  379. uint8_t *buf = this->plane0_;
  380. int local_x = x;
  381. if (this->desc_->window_per_plane && this->desc_->plane_count > 1) {
  382. stride = this->desc_->plane_width;
  383. if (x >= stride) {
  384. buf = this->plane1_;
  385. local_x = x - stride;
  386. }
  387. }
  388. uint32_t px = static_cast<uint32_t>(y) * stride + local_x;
  389. const uint8_t ppb = 8 / bpp;
  390. uint32_t idx = px / ppb;
  391. uint8_t shift = static_cast<uint8_t>((ppb - 1 - (px % ppb)) * bpp);
  392. uint8_t mask = static_cast<uint8_t>((1u << bpp) - 1);
  393. buf[idx] = static_cast<uint8_t>((buf[idx] & ~(mask << shift)) | ((code & mask) << shift));
  394. }
  395. void draw_absolute_pixel_internal(int x, int y, Color color) override {
  396. if (x < 0 || y < 0 || x >= this->desc_->width || y >= this->desc_->height)
  397. return;
  398. if (this->desc_->bits_per_pixel > 1) {
  399. this->draw_packed_(x, y, color);
  400. return;
  401. }
  402. bool red = color.red > color.green && color.red > color.blue && color.red > 0x7f;
  403. bool bright = color.red > 0x7f || color.green > 0x7f || color.blue > 0x7f;
  404. uint32_t idx = (static_cast<uint32_t>(y) * this->desc_->width + x) / 8;
  405. uint8_t mask = 0x80 >> (x % 8);
  406. // Red/accent pixels go to the color plane (when the panel has one),
  407. // everything else is black/white on the black plane. get_display_type()
  408. // reports
  409. // DISPLAY_TYPE_COLOR, so callers draw literal RGB colors, not the
  410. // on/off is_on() convention, even on a plane_count==1 (BW-only) panel -
  411. // there just is no color plane for a "red" classification to land on.
  412. //
  413. // Buffer convention here is 1=red/0=not-red (matching the reference
  414. // epaper_spi driver's EPaperWeAct3C internal convention), transformed
  415. // to the wire value by plane_invert[1] - see PANEL_213_Z98C's comment
  416. // for why that inversion flag and this buffer convention are coupled,
  417. // not independent facts.
  418. if (red && this->plane1_ != nullptr) {
  419. this->plane1_[idx] |= mask;
  420. } else {
  421. if (this->plane1_ != nullptr)
  422. this->plane1_[idx] &= ~mask;
  423. // plane0 bit set = white/blank. invert_mono_ flips the mono sense so a
  424. // bright color (e.g. ESPHome's default COLOR_ON) becomes ink, not blank
  425. // - see display.py's CONF_INVERT_MONO. Red classification is unaffected.
  426. if (bright ^ this->invert_mono_) {
  427. this->plane0_[idx] |= mask;
  428. } else {
  429. this->plane0_[idx] &= ~mask;
  430. }
  431. }
  432. }
  433. int get_width_internal() override { return this->desc_->width; }
  434. int get_height_internal() override { return this->desc_->height; }
  435. // The colors this panel can actually show, each as a representative RGB, in
  436. // a stable order (black, white, then accents). Packed panels (>1bpp): one
  437. // entry per distinct palette wire code, first RGB seen for each. 1bpp:
  438. // black + white, plus red when a second plane exists for the accent (only
  439. // red-classified pixels reach it - see draw_absolute_pixel_internal). This
  440. // is what makes test_pattern() bit-depth aware. Returns the count.
  441. int test_colors_(Color *out, int max) {
  442. int n = 0;
  443. if (this->desc_->bits_per_pixel > 1 && this->desc_->palette != nullptr) {
  444. uint8_t seen[16];
  445. int seen_n = 0;
  446. for (int i = 0; i < this->desc_->palette_len && n < max; i++) {
  447. const uint8_t code = this->desc_->palette[i].code;
  448. bool dup = false;
  449. for (int j = 0; j < seen_n; j++)
  450. if (seen[j] == code) { dup = true; break; }
  451. if (dup) continue;
  452. if (seen_n < (int) sizeof(seen)) seen[seen_n++] = code;
  453. const auto &e = this->desc_->palette[i];
  454. out[n++] = Color(e.r, e.g, e.b);
  455. }
  456. } else {
  457. if (n < max) out[n++] = Color(0x00, 0x00, 0x00); // black
  458. if (n < max) out[n++] = Color(0xFF, 0xFF, 0xFF); // white
  459. if (this->desc_->plane_count >= 2 && n < max) out[n++] = Color(0xFF, 0x00, 0x00); // accent
  460. }
  461. return n;
  462. }
  463. const EpdPanelDescriptor *desc_{nullptr};
  464. const char *model_name_{nullptr};
  465. bool verified_{false};
  466. GPIOPin *cs_{nullptr};
  467. GPIOPin *dc_{nullptr};
  468. GPIOPin *rst_{nullptr};
  469. GPIOPin *busy_{nullptr};
  470. EpdSPIDevice spi_;
  471. CycleEngine engine_;
  472. uint8_t *plane0_{nullptr};
  473. uint8_t *plane1_{nullptr};
  474. uint32_t plane_bytes_{0};
  475. bool invert_mono_{false};
  476. };
  477. } // namespace esphome::gxepd2