Просмотр исходного кода

Generalize pixel format to bits_per_pixel + palette; support Arduino

Replace the packed_2bpp_plane boolean with a bits_per_pixel field
(1 = 1bpp plane model, unchanged; 2/4 = packed code-per-pixel) plus a
per-panel palette (RGB -> wire code, nearest-color classified at draw
time, exact matches win). Buffer sizing is now one width*height*bpp/8
formula, and classification/packing is data-driven and depth-generic
(MSB-first, reduces exactly to the old 2bpp layout). A 4bpp 7-color
(ACeP) panel is now a palette + bits_per_pixel:4 with no new engine flag;
GXEPD2_PALETTE_4C/7C codes verified against vendor color4()/color7().
Byte-identical for the existing panels by construction.

Allow the Arduino framework in addition to ESP-IDF - the runtime C++ uses
only framework-portable ESPHome abstractions, so the old IDF-only gate was
a FreeRTOS-task-era leftover. Add tests/test_smoke.esp32-arduino.yaml;
both it (1bpp) and the IDF 2bpp-packed fixture compile clean.
ZiB 3 недель назад
Родитель
Сommit
4d5585f291

+ 2 - 2
components/gxepd2/cycle_engine.h

@@ -57,8 +57,8 @@ class CycleEngine {
     this->planes_[0] = plane0;
     this->planes_[1] = plane1;
     uint32_t w = this->desc_->window_per_plane ? this->desc_->plane_width : this->desc_->width;
-    this->plane_bytes_ = this->desc_->packed_2bpp_plane ? (w * this->desc_->height) / 4
-                                                         : (w * this->desc_->height) / 8;
+    // width*height*bpp/8: /8 at 1bpp, /4 at 2bpp, /2 at 4bpp.
+    this->plane_bytes_ = (uint32_t) w * this->desc_->height * this->desc_->bits_per_pixel / 8;
     this->bytes_per_row_ = this->plane_bytes_ / this->desc_->height;
     this->cursor_ = 0;
     this->plane_ = 0;

+ 7 - 1
components/gxepd2/display.py

@@ -57,7 +57,13 @@ CONFIG_SCHEMA = cv.All(
             cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema,
         }
     ),
-    cv.only_with_framework(Framework.ESP_IDF),
+    # IDF-first, but the runtime C++ uses only framework-portable ESPHome
+    # abstractions (spi::SPIDevice, GPIOPin, hal.h, RAMAllocator, ESP_LOG*) -
+    # the old IDF-only gate was a leftover from the superseded FreeRTOS-task
+    # design. ESP32-Arduino is a supported accommodation; ESP8266-Arduino
+    # compiles too but is resolution-restricted at 1bpp and lacks the PSRAM
+    # that >1bpp (color) panels need, so those fail allocation at runtime.
+    cv.only_with_framework([Framework.ESP_IDF, Framework.ARDUINO]),
 )
 
 

+ 63 - 11
components/gxepd2/epd_command.h

@@ -21,6 +21,46 @@ struct EpdCommand {
   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 GXEPD2_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 GXEPD2_PALETTE_4C_LEN = sizeof(GXEPD2_PALETTE_4C) / sizeof(GXEPD2_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.
+// Ready for the first 4bpp panel (GxEPD2_565c/730c); no shipped descriptor
+// consumes it yet - see the gxepd2 panel work.
+inline const EpdPaletteEntry GXEPD2_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 GXEPD2_PALETTE_7C_LEN = sizeof(GXEPD2_PALETTE_7C) / sizeof(GXEPD2_PALETTE_7C[0]);
+
 // Everything CycleEngine needs to drive one panel, hand-derived from a
 // GxEPD2 vendor driver's .cpp/.h (see panel_154_z90c.h). Only what this
 // first slice's engine actually uses - the RAM-window-set command layout
@@ -80,19 +120,31 @@ struct EpdPanelDescriptor {
   // reference driver (confirmed 2026-09-19: a bar/ladder/diagonal test
   // pattern came out vertically scrambled with a broken diagonal - the
   // documented signature of a row-reordering bug - sent in plain
-  // sequential order). Not assumed for any other packed_2bpp_plane panel
-  // (e.g. GDEY029F51, a single-chip panel with no master/slave split at
-  // all) without the same kind of hardware confirmation.
+  // sequential order). Not assumed for any other packed (bits_per_pixel > 1)
+  // panel (e.g. GDEY029F51, a single-chip panel with no master/slave split
+  // at all) without the same kind of hardware confirmation.
   bool row_interleave{false};
 
-  // true for the JD79667/HX8717-family 4-color panels (GDEY029F51,
-  // GDEY0579F51): pixels are 2 bits each, 4 per byte, built from a
-  // white/black/yellow/red classification at draw time - a fundamentally
-  // different buffer shape than the 1-bit-per-pixel plane(s) above, not
-  // just a different invert/command byte. When true, plane_invert is
-  // unused and GxEPD2Display's buffer(s) are width*height/4 bytes instead
-  // of /8 (per-plane width*height/4 when window_per_plane is also true).
-  bool packed_2bpp_plane{false};
+  // 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. This replaces the old packed_2bpp_plane bool -
+  // "is it packed" is now bits_per_pixel > 1, "which codes" is data, so a
+  // 4bpp 7-color panel is a palette + bits_per_pixel=4, no new engine flag.
+  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 GXEPD2_PALETTE_4C / GXEPD2_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;

+ 80 - 47
components/gxepd2/gxepd2_display.h

@@ -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

+ 7 - 4
components/gxepd2/panel_290c_gdey029f51.h

@@ -7,9 +7,9 @@
 // panels (0x00 PSR, 0x01 PWRR, 0x03 POFS, 0x06 BTST, 0x50 CDI, 0x60 TCON,
 // 0x61 TRES, 0x83 window-set, refresh via 0x50+0x12) and, more importantly,
 // a fundamentally different pixel format: 2 bits/pixel, 4 pixels/byte
-// (white=00, black=01, red=11), not 1-bit-per-pixel planes - see
-// EpdPanelDescriptor::packed_2bpp_plane and GxEPD2Display's
-// draw_absolute_pixel_internal for the buffer-side half of this.
+// (black=0x00, white=0x01, yellow=0x02, red=0x03 per GxEPD2_4C.h color4()),
+// not 1-bit-per-pixel planes - see EpdPanelDescriptor::bits_per_pixel /
+// palette and GxEPD2Display's draw_packed_() for the buffer-side half.
 //
 // _InitDisplay()'s reset pulse is itself gated on `_hibernating ||
 // _initial_write` in the vendor driver (steady-state cycles skip the
@@ -91,7 +91,10 @@ inline const EpdPanelDescriptor PANEL_290C_GDEY029F51 = {
     .plane_count = 1,
     .plane_write_cmd = {0x10, 0x00},
     .plane_invert = {false, false},
-    .packed_2bpp_plane = true,
+    .bits_per_pixel = 2,
+    .palette = GXEPD2_PALETTE_4C,
+    .palette_len = GXEPD2_PALETTE_4C_LEN,
+    .blank_code = 0x01,
     .refresh_sequence = PANEL_290C_GDEY029F51_REFRESH,
     .refresh_sequence_len = sizeof(PANEL_290C_GDEY029F51_REFRESH) / sizeof(PANEL_290C_GDEY029F51_REFRESH[0]),
 };

+ 4 - 1
components/gxepd2/panel_579c_gdey0579f51.h

@@ -129,7 +129,10 @@ inline const EpdPanelDescriptor PANEL_579C_GDEY0579F51 = {
     .plane_invert = {false, false},
     .plane_width = 396,  // each region's own local width
     .row_interleave = true,
-    .packed_2bpp_plane = true,
+    .bits_per_pixel = 2,
+    .palette = GXEPD2_PALETTE_4C,
+    .palette_len = GXEPD2_PALETTE_4C_LEN,
+    .blank_code = 0x01,
     .refresh_sequence = PANEL_579C_GDEY0579F51_REFRESH,
     .refresh_sequence_len = sizeof(PANEL_579C_GDEY0579F51_REFRESH) / sizeof(PANEL_579C_GDEY0579F51_REFRESH[0]),
 };

+ 4 - 1
components/gxepd2/panel_579c_gdey0579f51_master_only.h

@@ -116,7 +116,10 @@ inline const EpdPanelDescriptor PANEL_579C_GDEY0579F51_MASTER_ONLY = {
     .plane_write_cmd = {0x10, 0x00},
     .plane_invert = {false, false},
     .row_interleave = true,
-    .packed_2bpp_plane = true,
+    .bits_per_pixel = 2,
+    .palette = GXEPD2_PALETTE_4C,
+    .palette_len = GXEPD2_PALETTE_4C_LEN,
+    .blank_code = 0x01,
     .refresh_sequence = PANEL_579C_GDEY0579F51_MO_REFRESH,
     .refresh_sequence_len = sizeof(PANEL_579C_GDEY0579F51_MO_REFRESH) / sizeof(PANEL_579C_GDEY0579F51_MO_REFRESH[0]),
 };

+ 35 - 0
tests/test_smoke.esp32-arduino.yaml

@@ -0,0 +1,35 @@
+# Arduino-framework compile smoke test. The runtime is framework-portable
+# (see the gxepd2-support-targets memory / display.py's framework-gate
+# comment); this fixture exists to keep ESP32-Arduino building in CI/bench.
+# Uses a 1bpp panel deliberately - color (>1bpp) panels need PSRAM, which
+# plain esp32dev-arduino doesn't have.
+esphome:
+  name: gxepd2-smoke-arduino
+esp32:
+  board: esp32dev
+  framework:
+    type: arduino
+external_components:
+  - source:
+      type: local
+      path: ../components
+    components: [gxepd2]
+spi:
+  clk_pin: 18
+  mosi_pin: 23
+logger:
+display:
+  - platform: gxepd2
+    model: GxEPD2_213_Z98C
+    cs_pin: 5
+    dc_pin: 17
+    reset_pin: 16
+    busy_pin: 4
+    update_interval: 60s
+    lambda: |-
+      it.fill(Color::WHITE);
+      it.print(0, 0, id(roboto), Color::BLACK, "arduino smoke");
+font:
+  - file: "gfonts://Roboto"
+    id: roboto
+    size: 20