|
|
@@ -38,7 +38,8 @@ class GxEPD2Display : public display::DisplayBuffer {
|
|
|
// ESPHome draws into) - both regions are the same size here, so one
|
|
|
// plane_bytes_ value covers both buffers.
|
|
|
uint32_t w = desc->window_per_plane ? desc->plane_width : desc->width;
|
|
|
- this->plane_bytes_ = desc->packed_2bpp_plane ? (w * desc->height) / 4 : (w * desc->height) / 8;
|
|
|
+ // width*height*bpp/8: /8 at 1bpp, /4 at 2bpp (4-color), /2 at 4bpp (7-color).
|
|
|
+ this->plane_bytes_ = (uint32_t) w * desc->height * desc->bits_per_pixel / 8;
|
|
|
RAMAllocator<uint8_t> allocator;
|
|
|
this->plane0_ = allocator.allocate(this->plane_bytes_);
|
|
|
bool need_plane1 = desc->plane_count > 1;
|
|
|
@@ -48,15 +49,22 @@ class GxEPD2Display : public display::DisplayBuffer {
|
|
|
this->mark_failed();
|
|
|
return;
|
|
|
}
|
|
|
- // Packed 2bpp: code 0x01 (white) repeated 4x/byte = 0x55, matching
|
|
|
- // GxEPD2's own writeScreenBuffer()'s white fill value exactly - not
|
|
|
- // 0xFF (that's the 1bpp planes' white) or 0x00 (that's black here).
|
|
|
- memset(this->plane0_, desc->packed_2bpp_plane ? 0x55 : 0xFF, this->plane_bytes_);
|
|
|
- if (this->plane1_ != nullptr) {
|
|
|
- // plane1_ is a second packed region (master/slave) for a
|
|
|
- // window_per_plane panel - also defaults to white (0x55), not the
|
|
|
- // 1bpp "red" plane's 0=not-red convention.
|
|
|
- memset(this->plane1_, desc->packed_2bpp_plane ? 0x55 : 0x00, this->plane_bytes_);
|
|
|
+ if (desc->bits_per_pixel > 1) {
|
|
|
+ // Packed modes: clear every pixel to the panel's white/blank code,
|
|
|
+ // replicated across the byte - e.g. 4-color white=0x01 -> 0x55, 7-color
|
|
|
+ // white=0x01 -> 0x11, matching GxEPD2's own writeScreenBuffer() fill
|
|
|
+ // (color4()*0x55 / color7()|<<4). Both regions of a window_per_plane
|
|
|
+ // panel (master/slave) get the same blank fill.
|
|
|
+ uint8_t fill = replicate_code_(desc->blank_code, desc->bits_per_pixel);
|
|
|
+ memset(this->plane0_, fill, this->plane_bytes_);
|
|
|
+ if (this->plane1_ != nullptr)
|
|
|
+ memset(this->plane1_, fill, this->plane_bytes_);
|
|
|
+ } else {
|
|
|
+ // 1bpp plane model: plane0 white = all bits set (0xFF); plane1 (the
|
|
|
+ // accent/red plane, when present) = 0 = not-red.
|
|
|
+ memset(this->plane0_, 0xFF, this->plane_bytes_);
|
|
|
+ if (this->plane1_ != nullptr)
|
|
|
+ memset(this->plane1_, 0x00, this->plane_bytes_);
|
|
|
}
|
|
|
|
|
|
this->spi_.spi_setup();
|
|
|
@@ -85,48 +93,73 @@ class GxEPD2Display : public display::DisplayBuffer {
|
|
|
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
|
|
|
|
|
protected:
|
|
|
+ // Replicates a packed wire code across a whole byte (one code per pixel
|
|
|
+ // slot), e.g. 2bpp code 0x01 -> 0x55, 4bpp code 0x01 -> 0x11.
|
|
|
+ static uint8_t replicate_code_(uint8_t code, uint8_t bpp) {
|
|
|
+ uint8_t mask = static_cast<uint8_t>((1u << bpp) - 1);
|
|
|
+ uint8_t out = 0;
|
|
|
+ for (uint8_t i = 0; i < 8 / bpp; i++)
|
|
|
+ out = static_cast<uint8_t>((out << bpp) | (code & mask));
|
|
|
+ return out;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Maps a drawn RGB color to its panel wire code via the descriptor's
|
|
|
+ // palette: exact matches win (so named colors are deterministic, like
|
|
|
+ // GxEPD2's color4()/color7() switch), otherwise nearest in RGB space.
|
|
|
+ static uint8_t classify_(Color c, const EpdPaletteEntry *pal, uint8_t len) {
|
|
|
+ if (pal == nullptr || len == 0)
|
|
|
+ return 0;
|
|
|
+ uint8_t best = 0;
|
|
|
+ long best_d = -1;
|
|
|
+ for (uint8_t i = 0; i < len; i++) {
|
|
|
+ long dr = static_cast<long>(c.red) - pal[i].r;
|
|
|
+ long dg = static_cast<long>(c.green) - pal[i].g;
|
|
|
+ long db = static_cast<long>(c.blue) - pal[i].b;
|
|
|
+ long d = dr * dr + dg * dg + db * db;
|
|
|
+ if (d == 0)
|
|
|
+ return pal[i].code;
|
|
|
+ if (best_d < 0 || d < best_d) {
|
|
|
+ best_d = d;
|
|
|
+ best = i;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return pal[best].code;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Packed code-per-pixel model (bits_per_pixel > 1): N bits/pixel, MSB
|
|
|
+ // first, matching GxEPD2's own out_data packing in writeImage (4-color
|
|
|
+ // 4px/byte, 7-color 2px/byte). Routes to the correct region for a
|
|
|
+ // window_per_plane (master/slave) panel the same way the mono path does.
|
|
|
+ void draw_packed_(int x, int y, Color color) {
|
|
|
+ const uint8_t bpp = this->desc_->bits_per_pixel;
|
|
|
+ uint8_t code = classify_(color, this->desc_->palette, this->desc_->palette_len);
|
|
|
+ uint16_t stride = this->desc_->width;
|
|
|
+ uint8_t *buf = this->plane0_;
|
|
|
+ int local_x = x;
|
|
|
+ if (this->desc_->window_per_plane && this->desc_->plane_count > 1) {
|
|
|
+ stride = this->desc_->plane_width;
|
|
|
+ if (x >= stride) {
|
|
|
+ buf = this->plane1_;
|
|
|
+ local_x = x - stride;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ uint32_t px = static_cast<uint32_t>(y) * stride + local_x;
|
|
|
+ const uint8_t ppb = 8 / bpp;
|
|
|
+ uint32_t idx = px / ppb;
|
|
|
+ uint8_t shift = static_cast<uint8_t>((ppb - 1 - (px % ppb)) * bpp);
|
|
|
+ uint8_t mask = static_cast<uint8_t>((1u << bpp) - 1);
|
|
|
+ buf[idx] = static_cast<uint8_t>((buf[idx] & ~(mask << shift)) | ((code & mask) << shift));
|
|
|
+ }
|
|
|
+
|
|
|
void draw_absolute_pixel_internal(int x, int y, Color color) override {
|
|
|
if (x < 0 || y < 0 || x >= this->desc_->width || y >= this->desc_->height)
|
|
|
return;
|
|
|
- bool red = color.red > color.green && color.red > color.blue && color.red > 0x7f;
|
|
|
- bool bright = color.red > 0x7f || color.green > 0x7f || color.blue > 0x7f;
|
|
|
- if (this->desc_->packed_2bpp_plane) {
|
|
|
- // JD79667/HX8717-family panels: 2 bits/pixel, 4 pixels/byte, MSB
|
|
|
- // first (matches GxEPD2's own out_data <<= 2 packing order in e.g.
|
|
|
- // GxEPD2_290c_GDEY029F51::writeImage). Codes match GxEPD2_4C.h's own
|
|
|
- // color4() table exactly: black=0x00, white=0x01, yellow=0x02,
|
|
|
- // red=0x03 - confirmed against that table after first getting
|
|
|
- // black/white backwards by misreading writeImage's own
|
|
|
- // black_data-bit ternary (2026-09-19).
|
|
|
- bool yellow = color.red > 0x7f && color.green > 0x7f && color.blue <= 0x7f;
|
|
|
- uint8_t code;
|
|
|
- if (red)
|
|
|
- code = 0x03;
|
|
|
- else if (yellow)
|
|
|
- code = 0x02;
|
|
|
- else if (bright)
|
|
|
- code = 0x01;
|
|
|
- else
|
|
|
- code = 0x00;
|
|
|
- // window_per_plane (GDEY0579F51's master+slave): each region is its
|
|
|
- // own local 396-wide buffer, not one 792-wide one - route by which
|
|
|
- // half of the global x range this pixel falls in.
|
|
|
- uint16_t stride = this->desc_->width;
|
|
|
- uint8_t *buf = this->plane0_;
|
|
|
- int local_x = x;
|
|
|
- if (this->desc_->window_per_plane && this->desc_->plane_count > 1) {
|
|
|
- stride = this->desc_->plane_width;
|
|
|
- if (x >= stride) {
|
|
|
- buf = this->plane1_;
|
|
|
- local_x = x - stride;
|
|
|
- }
|
|
|
- }
|
|
|
- uint32_t px = static_cast<uint32_t>(y) * stride + local_x;
|
|
|
- uint32_t idx = px / 4;
|
|
|
- uint8_t shift = static_cast<uint8_t>((3 - (px % 4)) * 2);
|
|
|
- buf[idx] = static_cast<uint8_t>((buf[idx] & ~(0x03 << shift)) | (code << shift));
|
|
|
+ if (this->desc_->bits_per_pixel > 1) {
|
|
|
+ this->draw_packed_(x, y, color);
|
|
|
return;
|
|
|
}
|
|
|
+ bool red = color.red > color.green && color.red > color.blue && color.red > 0x7f;
|
|
|
+ bool bright = color.red > 0x7f || color.green > 0x7f || color.blue > 0x7f;
|
|
|
uint32_t idx = (static_cast<uint32_t>(y) * this->desc_->width + x) / 8;
|
|
|
uint8_t mask = 0x80 >> (x % 8);
|
|
|
// Same classification the old task-based component used: red/accent
|