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

Add packed-2bpp pixel format and two JD79667/HX8717 4-color descriptors

GxEPD2_290c_GDEY029F51 (single-region, full panel) and
GxEPD2_579c_GDEY0579F51_MASTER_ONLY (deliberately addresses only the
master/left half - the master+slave split needs a real CycleEngine
state-machine loop, scoped out for now per user decision).

CycleEngine itself needed no changes - a packed 2bpp buffer is still just
bytes to it. The new work is entirely in GxEPD2Display: a
packed_2bpp_plane descriptor flag switches buffer sizing (width*height/4)
and draw_absolute_pixel_internal to a 4-pixels/byte, 2-bit-per-pixel
classifier instead of the black/red 1bpp planes.

Codes (black=0x00, white=0x01, yellow=0x02, red=0x03) confirmed against
GxEPD2_4C.h's own color4() table after initially getting black/white
backwards from misreading writeImage's black-bit ternary - caught before
flashing, not on hardware this time.
ZiB 1 месяц назад
Родитель
Сommit
6c4ca34aeb

+ 5 - 0
components/gxepd2/display.py

@@ -37,6 +37,11 @@ MODELS = {
     "GxEPD2_213_Z98C": "PANEL_213_Z98C",
     "GxEPD2_213_Z98C": "PANEL_213_Z98C",
     "GxEPD2_426_GDEQ0426T82": "PANEL_426_GDEQ0426T82",
     "GxEPD2_426_GDEQ0426T82": "PANEL_426_GDEQ0426T82",
     "GxEPD2_583_GDEQ0583T31": "PANEL_583_GDEQ0583T31",
     "GxEPD2_583_GDEQ0583T31": "PANEL_583_GDEQ0583T31",
+    "GxEPD2_290c_GDEY029F51": "PANEL_290C_GDEY029F51",
+    # Master half only, as if it were a self-contained 396x272 panel - see
+    # panel_579c_gdey0579f51_master_only.h for why the master+slave split
+    # isn't wired up yet.
+    "GxEPD2_579c_GDEY0579F51_MASTER_ONLY": "PANEL_579C_GDEY0579F51_MASTER_ONLY",
 }
 }
 
 
 CONFIG_SCHEMA = cv.All(
 CONFIG_SCHEMA = cv.All(

+ 10 - 0
components/gxepd2/epd_command.h

@@ -53,6 +53,16 @@ struct EpdPanelDescriptor {
   uint8_t plane_write_cmd[2];   // e.g. {0x24, 0x26}
   uint8_t plane_write_cmd[2];   // e.g. {0x24, 0x26}
   bool plane_invert[2];         // true if the byte must be bit-inverted on write
   bool plane_invert[2];         // true if the byte must be bit-inverted on write
 
 
+  // true for the JD79667/HX8717-family 4-color panels (GDEY029F51,
+  // GDEY0579F51): pixels are 2 bits each, 4 per byte, built from a
+  // white/black/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_count/
+  // plane_invert are unused (always effectively 1 packed plane,
+  // plane_write_cmd[0] still names the write command) and
+  // GxEPD2Display's buffer is width*height/4 bytes instead of /8.
+  bool packed_2bpp_plane{false};
+
   const EpdCommand *refresh_sequence;  // e.g. 0x22/0xF7 then trigger 0x20
   const EpdCommand *refresh_sequence;  // e.g. 0x22/0xF7 then trigger 0x20
   uint8_t refresh_sequence_len;
   uint8_t refresh_sequence_len;
 };
 };

+ 37 - 6
components/gxepd2/gxepd2_display.h

@@ -6,7 +6,9 @@
 #include "esphome/components/spi/spi.h"
 #include "esphome/components/spi/spi.h"
 #include "cycle_engine.h"
 #include "cycle_engine.h"
 #include "panel_213_z98c.h"
 #include "panel_213_z98c.h"
+#include "panel_290c_gdey029f51.h"
 #include "panel_426_gdeq0426t82.h"
 #include "panel_426_gdeq0426t82.h"
+#include "panel_579c_gdey0579f51_master_only.h"
 #include "panel_583_gdeq0583t31.h"
 #include "panel_583_gdeq0583t31.h"
 
 
 namespace esphome::gxepd2 {
 namespace esphome::gxepd2 {
@@ -29,16 +31,21 @@ class GxEPD2Display : public display::DisplayBuffer {
 
 
   void setup() override {
   void setup() override {
     const auto *desc = this->desc_;
     const auto *desc = this->desc_;
-    this->plane_bytes_ = (static_cast<uint32_t>(desc->width) * desc->height) / 8;
+    this->plane_bytes_ = desc->packed_2bpp_plane ? (static_cast<uint32_t>(desc->width) * desc->height) / 4
+                                                  : (static_cast<uint32_t>(desc->width) * desc->height) / 8;
     RAMAllocator<uint8_t> allocator;
     RAMAllocator<uint8_t> allocator;
     this->plane0_ = allocator.allocate(this->plane_bytes_);
     this->plane0_ = allocator.allocate(this->plane_bytes_);
-    this->plane1_ = desc->plane_count > 1 ? allocator.allocate(this->plane_bytes_) : nullptr;
-    if (this->plane0_ == nullptr || (desc->plane_count > 1 && this->plane1_ == nullptr)) {
+    bool need_plane1 = !desc->packed_2bpp_plane && desc->plane_count > 1;
+    this->plane1_ = need_plane1 ? allocator.allocate(this->plane_bytes_) : nullptr;
+    if (this->plane0_ == nullptr || (need_plane1 && this->plane1_ == nullptr)) {
       ESP_LOGE(TAG, "Failed to allocate %u-byte plane buffer(s)", (unsigned) this->plane_bytes_);
       ESP_LOGE(TAG, "Failed to allocate %u-byte plane buffer(s)", (unsigned) this->plane_bytes_);
       this->mark_failed();
       this->mark_failed();
       return;
       return;
     }
     }
-    memset(this->plane0_, 0xFF, this->plane_bytes_);  // 1 = white
+    // 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)
     if (this->plane1_ != nullptr)
       memset(this->plane1_, 0x00, this->plane_bytes_);  // 0 = not-red
       memset(this->plane1_, 0x00, this->plane_bytes_);  // 0 = not-red
 
 
@@ -71,6 +78,32 @@ class GxEPD2Display : public display::DisplayBuffer {
   void draw_absolute_pixel_internal(int x, int y, Color color) override {
   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)
     if (x < 0 || y < 0 || x >= this->desc_->width || y >= this->desc_->height)
       return;
       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;
+      uint32_t px = static_cast<uint32_t>(y) * this->desc_->width + x;
+      uint32_t idx = px / 4;
+      uint8_t shift = static_cast<uint8_t>((3 - (px % 4)) * 2);
+      this->plane0_[idx] = static_cast<uint8_t>((this->plane0_[idx] & ~(0x03 << shift)) | (code << shift));
+      return;
+    }
     uint32_t idx = (static_cast<uint32_t>(y) * this->desc_->width + x) / 8;
     uint32_t idx = (static_cast<uint32_t>(y) * this->desc_->width + x) / 8;
     uint8_t mask = 0x80 >> (x % 8);
     uint8_t mask = 0x80 >> (x % 8);
     // Same classification the old task-based component used: red/accent
     // Same classification the old task-based component used: red/accent
@@ -85,8 +118,6 @@ class GxEPD2Display : public display::DisplayBuffer {
     // to the wire value by plane_invert[1] - see PANEL_213_Z98C's comment
     // to the wire value by plane_invert[1] - see PANEL_213_Z98C's comment
     // for why that inversion flag and this buffer convention are coupled,
     // for why that inversion flag and this buffer convention are coupled,
     // not independent facts.
     // not independent facts.
-    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 (red && this->plane1_ != nullptr) {
     if (red && this->plane1_ != nullptr) {
       this->plane1_[idx] |= mask;
       this->plane1_[idx] |= mask;
     } else {
     } else {

+ 99 - 0
components/gxepd2/panel_290c_gdey029f51.h

@@ -0,0 +1,99 @@
+#pragma once
+
+// Hand-derived from vendor/src/epd4c/GxEPD2_290c_GDEY029F51.{h,cpp}
+// (GDEY029F51, JD79667 controller, 128x296, 4-color BWRY panel used here as
+// black/white/red via the black+color buffer interface). First panel on a
+// completely different command set from the SSD1680/1677/UC8179 mono
+// 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.
+//
+// _InitDisplay()'s reset pulse is itself gated on `_hibernating ||
+// _initial_write` in the vendor driver (steady-state cycles skip the
+// physical RST and go straight to re-issuing the command sequence) - this
+// engine always does a real hardware reset every cycle regardless (see
+// CycleEngine::request_full_update()), a superset of what the vendor
+// driver does on its own first cycle, so no special-casing needed here.
+
+#include "epd_command.h"
+
+namespace esphome::gxepd2 {
+
+inline const uint8_t PANEL_290C_GDEY029F51_CMD_4D[] = {0x78};
+inline const uint8_t PANEL_290C_GDEY029F51_PSR[] = {0x0F, 0x29};
+inline const uint8_t PANEL_290C_GDEY029F51_PWRR[] = {0x07, 0x00};
+inline const uint8_t PANEL_290C_GDEY029F51_POFS[] = {0x10, 0x54, 0x44};
+inline const uint8_t PANEL_290C_GDEY029F51_BTST[] = {0x05, 0x00, 0x3F, 0x0A, 0x25, 0x12, 0x1A};
+inline const uint8_t PANEL_290C_GDEY029F51_CDI[] = {0x37};
+inline const uint8_t PANEL_290C_GDEY029F51_TCON[] = {0x02, 0x02};
+inline const uint8_t PANEL_290C_GDEY029F51_TRES[] = {0x00, 0x80, 0x01, 0x28};  // WIDTH=128, HEIGHT=296
+inline const uint8_t PANEL_290C_GDEY029F51_E7[] = {0x1C};
+inline const uint8_t PANEL_290C_GDEY029F51_PWS[] = {0x22};
+inline const uint8_t PANEL_290C_GDEY029F51_LVD[] = {0xD0};
+inline const uint8_t PANEL_290C_GDEY029F51_B5[] = {0x03};
+inline const uint8_t PANEL_290C_GDEY029F51_E9[] = {0x01};
+inline const uint8_t PANEL_290C_GDEY029F51_PLL[] = {0x08};
+
+inline const EpdCommand PANEL_290C_GDEY029F51_INIT[] = {
+    {0x4D, PANEL_290C_GDEY029F51_CMD_4D, 1},
+    {0x00, PANEL_290C_GDEY029F51_PSR, 2},
+    {0x01, PANEL_290C_GDEY029F51_PWRR, 2},
+    {0x03, PANEL_290C_GDEY029F51_POFS, 3},
+    {0x06, PANEL_290C_GDEY029F51_BTST, 7},
+    {0x50, PANEL_290C_GDEY029F51_CDI, 1},
+    {0x60, PANEL_290C_GDEY029F51_TCON, 2},
+    {0x61, PANEL_290C_GDEY029F51_TRES, 4},
+    {0xE7, PANEL_290C_GDEY029F51_E7, 1},
+    {0xE3, PANEL_290C_GDEY029F51_PWS, 1},
+    {0xB4, PANEL_290C_GDEY029F51_LVD, 1},
+    {0xB5, PANEL_290C_GDEY029F51_B5, 1},
+    {0xE9, PANEL_290C_GDEY029F51_E9, 1},
+    {0x30, PANEL_290C_GDEY029F51_PLL, 1},
+    {0x04, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},  // power on
+};
+
+// _setPartialRamArea(0, 0, 128, 296), default partial_mode=true.
+inline const uint8_t PANEL_290C_GDEY029F51_WINDOW[] = {
+    0x00, 0x00,  // x/256, x%256   = 0
+    0x00, 0x7F,  // xe/256, xe%256 = 127
+    0x00, 0x00,  // y/256, y%256   = 0
+    0x01, 0x27,  // ye/256, ye%256 = 295
+    0x01,
+};
+
+inline const EpdCommand PANEL_290C_GDEY029F51_WINDOW_SEQ[] = {
+    {0x83, PANEL_290C_GDEY029F51_WINDOW, 9},
+};
+
+// _refresh(false): 0x50=0x37 (white/non-floating border, not partial),
+// then 0x12+delay(1)+_waitWhileBusy - collapsed into one wait_busy_after.
+inline const uint8_t PANEL_290C_GDEY029F51_CDI_REFRESH[] = {0x37};
+inline const uint8_t PANEL_290C_GDEY029F51_REFRESH_TRIGGER[] = {0x00};
+
+inline const EpdCommand PANEL_290C_GDEY029F51_REFRESH[] = {
+    {0x50, PANEL_290C_GDEY029F51_CDI_REFRESH, 1},
+    {0x12, PANEL_290C_GDEY029F51_REFRESH_TRIGGER, 1, /*delay_before_ms=*/0, /*delay_after_ms=*/0,
+     /*wait_busy_after=*/true},
+};
+
+inline const EpdPanelDescriptor PANEL_290C_GDEY029F51 = {
+    .width = 128,
+    .height = 296,
+    .busy_active_level = false,  // GxEPD2_EPD(..., /*busy_level=*/LOW, ...)
+    .busy_timeout_ms = 30000,    // full_refresh_time is 25000ms
+    .init_sequence = PANEL_290C_GDEY029F51_INIT,
+    .init_sequence_len = sizeof(PANEL_290C_GDEY029F51_INIT) / sizeof(PANEL_290C_GDEY029F51_INIT[0]),
+    .window_sequence = PANEL_290C_GDEY029F51_WINDOW_SEQ,
+    .window_sequence_len = sizeof(PANEL_290C_GDEY029F51_WINDOW_SEQ) / sizeof(PANEL_290C_GDEY029F51_WINDOW_SEQ[0]),
+    .plane_count = 1,
+    .plane_write_cmd = {0x10, 0x00},
+    .plane_invert = {false, false},
+    .packed_2bpp_plane = true,
+    .refresh_sequence = PANEL_290C_GDEY029F51_REFRESH,
+    .refresh_sequence_len = sizeof(PANEL_290C_GDEY029F51_REFRESH) / sizeof(PANEL_290C_GDEY029F51_REFRESH[0]),
+};
+
+}  // namespace esphome::gxepd2

+ 102 - 0
components/gxepd2/panel_579c_gdey0579f51_master_only.h

@@ -0,0 +1,102 @@
+#pragma once
+
+// Hand-derived from vendor/src/epd4c/GxEPD2_579c_GDEY0579F51.{h,cpp}
+// (GDEY0579F51, HX8717 controller, 792x272, same JD79667-family 2bpp/4-color
+// packing as GDEY029F51 - see panel_290c_gdey029f51.h). This is the actual
+// dual-CS-like split panel from the original bench list: NOT two physical
+// CS pins, but two logical regions ("master"/"slave", left/right halves)
+// selected over one bus via an 0xA2 command, each with its own window-set
+// and its own 0x10 plane-write burst.
+//
+// Deliberate scope cut (2026-09-19, per user): CycleEngine's state machine
+// only knows one window-set + one plane-write per update cycle, and
+// extending it to loop over two regions is real work, not just descriptor
+// data - so this descriptor addresses the master half ONLY, as if it were
+// a self-contained 396x272 single-region panel, to validate the 2bpp
+// packing and this controller family's init/refresh shape before building
+// the master+slave loop. Driving only the left half of the physical
+// screen is the expected/correct result of this descriptor, not a bug.
+// The slave half (right half) is simply never written or refreshed here.
+//
+// _InitDisplay()'s reset pulse is gated the same way GDEY029F51's is
+// (`_hibernating || _initial_write`) - this engine always does a real
+// hardware reset every cycle regardless, a superset of the vendor
+// driver's own first-cycle behavior, so no special-casing needed.
+
+#include "epd_command.h"
+
+namespace esphome::gxepd2 {
+
+inline const uint8_t PANEL_579C_GDEY0579F51_MO_SELECT_MASTER[] = {0x01};
+inline const uint8_t PANEL_579C_GDEY0579F51_MO_SELECT_BOTH[] = {0x00};
+inline const uint8_t PANEL_579C_GDEY0579F51_MO_PSR_MASTER[] = {0x07, 0x29};
+inline const uint8_t PANEL_579C_GDEY0579F51_MO_BTST[] = {0x32, 0x32, 0x32, 0x32};
+inline const uint8_t PANEL_579C_GDEY0579F51_MO_TRES[] = {0x01, 0x8C, 0x01, 0x10};  // 396x272
+inline const uint8_t PANEL_579C_GDEY0579F51_MO_CDI_INIT[] = {0x97};
+inline const uint8_t PANEL_579C_GDEY0579F51_MO_E9[] = {0x01};
+inline const uint8_t PANEL_579C_GDEY0579F51_MO_E0[] = {0x01};
+
+inline const EpdCommand PANEL_579C_GDEY0579F51_MO_INIT[] = {
+    {0xA2, PANEL_579C_GDEY0579F51_MO_SELECT_MASTER, 1},
+    {0x00, PANEL_579C_GDEY0579F51_MO_PSR_MASTER, 2},
+    {0xA2, PANEL_579C_GDEY0579F51_MO_SELECT_BOTH, 1},
+    {0x06, PANEL_579C_GDEY0579F51_MO_BTST, 4},
+    {0x61, PANEL_579C_GDEY0579F51_MO_TRES, 4},
+    {0x50, PANEL_579C_GDEY0579F51_MO_CDI_INIT, 1},
+    {0xE9, PANEL_579C_GDEY0579F51_MO_E9, 1},
+    {0xE0, PANEL_579C_GDEY0579F51_MO_E0, 1},
+    {0xA2, PANEL_579C_GDEY0579F51_MO_SELECT_MASTER, 1},
+    {0x04, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},  // power on
+};
+
+// _setPartialRamAreaMaster(0, 0, 396, 272), default partial_mode=true.
+inline const uint8_t PANEL_579C_GDEY0579F51_MO_WINDOW[] = {
+    0x00, 0x00,  // x/256, x%256   = 0
+    0x01, 0x8B,  // xe/256, xe%256 = 395
+    0x00, 0x00,  // y/256, y%256   = 0
+    0x01, 0x0F,  // ye/256, ye%256 = 271
+    0x01,
+};
+
+inline const EpdCommand PANEL_579C_GDEY0579F51_MO_WINDOW_SEQ[] = {
+    {0xA2, PANEL_579C_GDEY0579F51_MO_SELECT_MASTER, 1},
+    {0x83, PANEL_579C_GDEY0579F51_MO_WINDOW, 9},
+};
+
+// refresh(false): _setPartialRamAreaBoth(0,0,396,272) (select=Both, same
+// region, harmless even though only master was ever written), then
+// _refresh(false): 0x50=0x37, 0x12+delay(1)+busywait collapsed into one
+// wait_busy_after.
+inline const uint8_t PANEL_579C_GDEY0579F51_MO_WINDOW_BOTH[] = {
+    0x00, 0x00, 0x01, 0x8B, 0x00, 0x00, 0x01, 0x0F, 0x01,
+};
+inline const uint8_t PANEL_579C_GDEY0579F51_MO_CDI_REFRESH[] = {0x37};
+inline const uint8_t PANEL_579C_GDEY0579F51_MO_REFRESH_TRIGGER[] = {0x00};
+
+inline const EpdCommand PANEL_579C_GDEY0579F51_MO_REFRESH[] = {
+    {0xA2, PANEL_579C_GDEY0579F51_MO_SELECT_BOTH, 1},
+    {0x83, PANEL_579C_GDEY0579F51_MO_WINDOW_BOTH, 9},
+    {0x50, PANEL_579C_GDEY0579F51_MO_CDI_REFRESH, 1},
+    {0x12, PANEL_579C_GDEY0579F51_MO_REFRESH_TRIGGER, 1, /*delay_before_ms=*/0, /*delay_after_ms=*/0,
+     /*wait_busy_after=*/true},
+};
+
+inline const EpdPanelDescriptor PANEL_579C_GDEY0579F51_MASTER_ONLY = {
+    .width = 396,   // WIDTH/2 of the real 792-wide panel - master half only
+    .height = 272,
+    .busy_active_level = false,  // GxEPD2_EPD(..., /*busy_level=*/LOW, ...)
+    .busy_timeout_ms = 25000,    // full_refresh_time is 21000ms
+    .init_sequence = PANEL_579C_GDEY0579F51_MO_INIT,
+    .init_sequence_len = sizeof(PANEL_579C_GDEY0579F51_MO_INIT) / sizeof(PANEL_579C_GDEY0579F51_MO_INIT[0]),
+    .window_sequence = PANEL_579C_GDEY0579F51_MO_WINDOW_SEQ,
+    .window_sequence_len =
+        sizeof(PANEL_579C_GDEY0579F51_MO_WINDOW_SEQ) / sizeof(PANEL_579C_GDEY0579F51_MO_WINDOW_SEQ[0]),
+    .plane_count = 1,
+    .plane_write_cmd = {0x10, 0x00},
+    .plane_invert = {false, false},
+    .packed_2bpp_plane = true,
+    .refresh_sequence = PANEL_579C_GDEY0579F51_MO_REFRESH,
+    .refresh_sequence_len = sizeof(PANEL_579C_GDEY0579F51_MO_REFRESH) / sizeof(PANEL_579C_GDEY0579F51_MO_REFRESH[0]),
+};
+
+}  // namespace esphome::gxepd2

+ 25 - 0
tests/test_gxepd2_290c_gdey029f51_native.esp32-idf.yaml

@@ -0,0 +1,25 @@
+esphome:
+  name: gxepd2-test-290c-gdey029f51
+esp32:
+  board: esp32dev
+  framework:
+    type: esp-idf
+external_components:
+  - source:
+      type: local
+      path: ../components
+    components: [gxepd2]
+spi:
+  clk_pin: 18
+  mosi_pin: 23
+logger:
+display:
+  - platform: gxepd2
+    model: GxEPD2_290c_GDEY029F51
+    cs_pin: 5
+    dc_pin: 17
+    reset_pin: 16
+    busy_pin: 4
+    update_interval: 60s
+    lambda: |-
+      it.fill(Color::WHITE);

+ 25 - 0
tests/test_gxepd2_579c_gdey0579f51_master_only.esp32-idf.yaml

@@ -0,0 +1,25 @@
+esphome:
+  name: gxepd2-test-579c-master-only
+esp32:
+  board: esp32dev
+  framework:
+    type: esp-idf
+external_components:
+  - source:
+      type: local
+      path: ../components
+    components: [gxepd2]
+spi:
+  clk_pin: 18
+  mosi_pin: 23
+logger:
+display:
+  - platform: gxepd2
+    model: GxEPD2_579c_GDEY0579F51_MASTER_ONLY
+    cs_pin: 5
+    dc_pin: 17
+    reset_pin: 16
+    busy_pin: 4
+    update_interval: 60s
+    lambda: |-
+      it.fill(Color::WHITE);