|
|
@@ -30,6 +30,124 @@ class GxEPD2Display : public display::DisplayBuffer {
|
|
|
void set_spi_parent(spi::SPIComponent *parent) { this->spi_.set_spi_parent(parent); }
|
|
|
void set_descriptor(const EpdPanelDescriptor *desc) { this->desc_ = desc; }
|
|
|
void set_invert_mono(bool invert) { this->invert_mono_ = invert; }
|
|
|
+ // Model trailer (e.g. "GDEQ0583T31", not the full "GxEPD2_583_..." id) and
|
|
|
+ // an optional font, both for test_pattern()'s on-screen panel label. Set
|
|
|
+ // from display.py: the name is derived from model:, the font is a normal
|
|
|
+ // ESPHome font: the builder can pull from gfonts://. See test_pattern().
|
|
|
+ void set_model_name(const char *name) { this->model_name_ = name; }
|
|
|
+ void set_name_font(display::BaseFont *font) { this->name_font_ = font; }
|
|
|
+
|
|
|
+ // Bench visual-check pattern, bit-depth aware. Everything scales to what
|
|
|
+ // THIS panel can actually show (see test_colors_): the color-bar count,
|
|
|
+ // the ladder color cycle and the corner markers all follow the panel's
|
|
|
+ // real color set instead of a hardcoded 4, so the drawn pattern doubles as
|
|
|
+ // a statement of what the driver expects of the panel. Adds a dithered
|
|
|
+ // grey ramp (so a 1bpp panel shows a graded sweep, not just two flat
|
|
|
+ // blocks) and, when a name font is set and the panel can spare the room, a
|
|
|
+ // model-trailer label so a tested panel says what it is on the shelf.
|
|
|
+ // Call it from the display lambda: `id(<display_id>).test_pattern();`.
|
|
|
+ void test_pattern() {
|
|
|
+ const int w = this->get_width();
|
|
|
+ const int h = this->get_height();
|
|
|
+ Color colors[8];
|
|
|
+ const int n = this->test_colors_(colors, 8);
|
|
|
+
|
|
|
+ this->fill(Color(0xFF, 0xFF, 0xFF));
|
|
|
+
|
|
|
+ const int smin = w < h ? w : h;
|
|
|
+ const int border = smin / 100 > 1 ? smin / 100 : 1;
|
|
|
+ const int x0 = border;
|
|
|
+ int y0 = border;
|
|
|
+ const int iw = w - 2 * border;
|
|
|
+ int ih = h - 2 * border;
|
|
|
+
|
|
|
+ // Name band, above every test zone. Drawn only when a font is set and
|
|
|
+ // the panel is tall enough to give it a fifth of the interior without
|
|
|
+ // crowding the zones below - the "skip the name on panels too short to
|
|
|
+ // spare ~10px above the bar" rule, sized to the actual font.
|
|
|
+ if (this->name_font_ != nullptr && this->model_name_ != nullptr && this->model_name_[0] != '\0') {
|
|
|
+ int tw, xoff, baseline, th;
|
|
|
+ this->name_font_->measure(this->model_name_, &tw, &xoff, &baseline, &th);
|
|
|
+ const int band = th + 4;
|
|
|
+ if (band <= ih / 5) {
|
|
|
+ this->print(w / 2, y0 + band / 2, this->name_font_, Color(0x00, 0x00, 0x00),
|
|
|
+ display::TextAlign::CENTER, this->model_name_);
|
|
|
+ y0 += band;
|
|
|
+ ih -= band;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Vertical budget for the four zones below any name band.
|
|
|
+ const int bars_h = ih * 34 / 100;
|
|
|
+ int dither_h = ih * 12 / 100;
|
|
|
+ if (dither_h < 8) dither_h = 8;
|
|
|
+ const int row_h = ih * 18 / 100 > 2 ? ih * 18 / 100 : 2;
|
|
|
+ const int dy = y0 + bars_h;
|
|
|
+ const int ry = dy + dither_h;
|
|
|
+ const int col_y = ry + row_h;
|
|
|
+ const int col_h = (y0 + ih) - col_y;
|
|
|
+
|
|
|
+ // Color bars: one solid block per available color, in palette/wire-code
|
|
|
+ // order (black, white, then the accents). A wrong or missing block
|
|
|
+ // points straight at the color - or the wire code - it stands for.
|
|
|
+ const int bar_w = iw / n;
|
|
|
+ for (int i = 0; i < n; i++) {
|
|
|
+ const int bx = x0 + i * bar_w;
|
|
|
+ const int bw = (i == n - 1) ? (x0 + iw - bx) : bar_w;
|
|
|
+ this->filled_rectangle(bx, y0, bw, bars_h, colors[i]);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Dithered grey ramp: brightness 255 (left) -> 0 (right) via an ordered
|
|
|
+ // 4x4 Bayer threshold. On a 1bpp panel this is the only zone that shows
|
|
|
+ // a graded sweep; on deeper panels it's a quick mono/uniformity check.
|
|
|
+ static const uint8_t BAYER4[4][4] = {
|
|
|
+ {0, 8, 2, 10}, {12, 4, 14, 6}, {3, 11, 1, 9}, {15, 7, 13, 5}};
|
|
|
+ for (int yy = 0; yy < dither_h; yy++) {
|
|
|
+ for (int xx = 0; xx < iw; xx++) {
|
|
|
+ const int bright = (iw <= 1) ? 255 : 255 - xx * 255 / (iw - 1);
|
|
|
+ const int thr = (BAYER4[yy & 3][xx & 3] * 2 + 1) * 255 / 32;
|
|
|
+ if (bright <= thr)
|
|
|
+ this->draw_pixel_at(x0 + xx, dy + yy, Color(0x00, 0x00, 0x00));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Row ladder: 1px rows cycling the full color set - a dropped, doubled
|
|
|
+ // or reordered row breaks the N-row cadence.
|
|
|
+ for (int yy = 0; yy < row_h; yy++)
|
|
|
+ this->horizontal_line(x0, ry + yy, iw, colors[yy % n]);
|
|
|
+
|
|
|
+ // Column ladder: the same rotated 90 degrees, catching nibble/column
|
|
|
+ // order bugs the row ladder can't (packed formats put several pixels per
|
|
|
+ // byte, so a column-order fault can leave every row looking fine).
|
|
|
+ for (int xx = 0; xx < iw; xx++)
|
|
|
+ this->vertical_line(x0 + xx, col_y, col_h, colors[xx % n]);
|
|
|
+
|
|
|
+ // Corner markers for orientation, using the panel's ink colors (every
|
|
|
+ // color but white). BL is always black; TL/TR take the next inks when
|
|
|
+ // the panel has them; BR is left blank as the distinct "no marker"
|
|
|
+ // state. A 90/180 rotation or mirror can't survive this even though the
|
|
|
+ // bars and ladders are otherwise symmetric.
|
|
|
+ Color ink[8];
|
|
|
+ int ni = 0;
|
|
|
+ for (int i = 0; i < n; i++) {
|
|
|
+ if (colors[i].red == 0xFF && colors[i].green == 0xFF && colors[i].blue == 0xFF)
|
|
|
+ continue; // skip white - a white marker is invisible
|
|
|
+ ink[ni++] = colors[i];
|
|
|
+ }
|
|
|
+ const int mk = smin / 12 > 3 ? smin / 12 : 3;
|
|
|
+ if (ni > 0) this->filled_rectangle(x0, y0 + ih - mk, mk, mk, ink[0]); // BL
|
|
|
+ if (ni > 1) this->filled_rectangle(x0, y0, mk, mk, ink[1]); // TL
|
|
|
+ if (ni > 2) this->filled_rectangle(x0 + iw - mk, y0, mk, mk, ink[2]); // TR
|
|
|
+
|
|
|
+ // Corner-to-corner diagonals over everything: the cheapest coordinate-
|
|
|
+ // transform check - a clean line means no row/column reordering.
|
|
|
+ this->line(0, 0, w - 1, h - 1, Color(0x00, 0x00, 0x00));
|
|
|
+ this->line(w - 1, 0, 0, h - 1, Color(0x00, 0x00, 0x00));
|
|
|
+
|
|
|
+ // Border frame last, so it stays an unbroken rectangle on all four edges
|
|
|
+ // even where a zone fills to the edge - catches a clipped/offset origin.
|
|
|
+ this->rectangle(0, 0, w, h, Color(0x00, 0x00, 0x00));
|
|
|
+ }
|
|
|
|
|
|
void setup() override {
|
|
|
const auto *desc = this->desc_;
|
|
|
@@ -200,7 +318,38 @@ class GxEPD2Display : public display::DisplayBuffer {
|
|
|
int get_width_internal() override { return this->desc_->width; }
|
|
|
int get_height_internal() override { return this->desc_->height; }
|
|
|
|
|
|
+ // The colors this panel can actually show, each as a representative RGB, in
|
|
|
+ // a stable order (black, white, then accents). Packed panels (>1bpp): one
|
|
|
+ // entry per distinct palette wire code, first RGB seen for each. 1bpp:
|
|
|
+ // black + white, plus red when a second plane exists for the accent (only
|
|
|
+ // red-classified pixels reach it - see draw_absolute_pixel_internal). This
|
|
|
+ // is what makes test_pattern() bit-depth aware. Returns the count.
|
|
|
+ int test_colors_(Color *out, int max) {
|
|
|
+ int n = 0;
|
|
|
+ if (this->desc_->bits_per_pixel > 1 && this->desc_->palette != nullptr) {
|
|
|
+ uint8_t seen[16];
|
|
|
+ int seen_n = 0;
|
|
|
+ for (int i = 0; i < this->desc_->palette_len && n < max; i++) {
|
|
|
+ const uint8_t code = this->desc_->palette[i].code;
|
|
|
+ bool dup = false;
|
|
|
+ for (int j = 0; j < seen_n; j++)
|
|
|
+ if (seen[j] == code) { dup = true; break; }
|
|
|
+ if (dup) continue;
|
|
|
+ if (seen_n < (int) sizeof(seen)) seen[seen_n++] = code;
|
|
|
+ const auto &e = this->desc_->palette[i];
|
|
|
+ out[n++] = Color(e.r, e.g, e.b);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (n < max) out[n++] = Color(0x00, 0x00, 0x00); // black
|
|
|
+ if (n < max) out[n++] = Color(0xFF, 0xFF, 0xFF); // white
|
|
|
+ if (this->desc_->plane_count >= 2 && n < max) out[n++] = Color(0xFF, 0x00, 0x00); // accent
|
|
|
+ }
|
|
|
+ return n;
|
|
|
+ }
|
|
|
+
|
|
|
const EpdPanelDescriptor *desc_{nullptr};
|
|
|
+ const char *model_name_{nullptr};
|
|
|
+ display::BaseFont *name_font_{nullptr};
|
|
|
GPIOPin *cs_{nullptr};
|
|
|
GPIOPin *dc_{nullptr};
|
|
|
GPIOPin *rst_{nullptr};
|