From 88cfdc33d458475c4dc3fa67653f7abf355dd7f0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Michael=20Gr=C3=BCner?= <michael.gruener@chaosmoon.net>
Date: Wed, 12 Feb 2025 00:17:34 +0100
Subject: [PATCH] GDEY042T81 e-paper displays support (#8061)

Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
---
 .../components/waveshare_epaper/display.py    |   2 +
 .../waveshare_epaper/waveshare_epaper.cpp     | 200 ++++++++++++++++++
 .../waveshare_epaper/waveshare_epaper.h       |  37 ++++
 tests/components/waveshare_epaper/common.yaml |  20 ++
 4 files changed, 259 insertions(+)

diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py
index 7b51eb338c..72823e4e36 100644
--- a/esphome/components/waveshare_epaper/display.py
+++ b/esphome/components/waveshare_epaper/display.py
@@ -56,6 +56,7 @@ GDEY029T94 = waveshare_epaper_ns.class_("GDEY029T94", WaveshareEPaper)
 WaveshareEPaper2P9InDKE = waveshare_epaper_ns.class_(
     "WaveshareEPaper2P9InDKE", WaveshareEPaper
 )
+GDEY042T81 = waveshare_epaper_ns.class_("GDEY042T81", WaveshareEPaper)
 WaveshareEPaper2P9InD = waveshare_epaper_ns.class_(
     "WaveshareEPaper2P9InD", WaveshareEPaper
 )
@@ -141,6 +142,7 @@ MODELS = {
     "2.90inv2-r2": ("c", WaveshareEPaper2P9InV2R2),
     "2.90in-d": ("b", WaveshareEPaper2P9InD),
     "2.90in-dke": ("c", WaveshareEPaper2P9InDKE),
+    "gdey042t81": ("c", GDEY042T81),
     "4.20in": ("b", WaveshareEPaper4P2In),
     "4.20in-bv2": ("b", WaveshareEPaper4P2InBV2),
     "4.20in-bv2-bwr": ("b", WaveshareEPaper4P2InBV2BWR),
diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp
index 1e8671bfa6..2a540a1b75 100644
--- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp
+++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp
@@ -2169,6 +2169,206 @@ void GDEW0154M09::dump_config() {
   LOG_UPDATE_INTERVAL(this);
 }
 
+// ========================================================
+//     Good Display 4.2in black/white GDEY042T81 (SSD1683)
+// Product page:
+//  - https://www.good-display.com/product/386.html
+// Datasheet:
+//  - https://v4.cecdn.yun300.cn/100001_1909185148/GDEY042T81.pdf
+//  - https://v4.cecdn.yun300.cn/100001_1909185148/SSD1683.PDF
+// Reference code from GoodDisplay:
+//  - https://www.good-display.com/companyfile/1572.html (2024-08-01 15:40:41)
+// Other reference code:
+//  - https://github.com/ZinggJM/GxEPD2/blob/03d8e7a533c1493f762e392ead12f1bcb7fab8f9/src/gdey/GxEPD2_420_GDEY042T81.cpp
+// ========================================================
+
+void GDEY042T81::initialize() {
+  this->init_display_();
+  ESP_LOGD(TAG, "Initialization complete, set the display to deep sleep");
+  this->deep_sleep();
+}
+
+// conflicting documentation / examples regarding reset timings
+//   https://v4.cecdn.yun300.cn/100001_1909185148/SSD1683.PDF -> 10ms
+//   GD sample code (Display_EPD_W21.cpp, see above) -> 10 ms
+//   https://v4.cecdn.yun300.cn/100001_1909185148/GDEY042T81.pdf (section 14.2) -> 0.2ms (200us)
+//   https://github.com/ZinggJM/GxEPD2/blob/03d8e7a533c1493f762e392ead12f1bcb7fab8f9/src/gdey/GxEPD2_420_GDEY042T81.cpp#L351
+//   -> 10ms
+//  10 ms seems to work, so we use this
+GDEY042T81::GDEY042T81() { this->reset_duration_ = 10; }
+
+void GDEY042T81::reset_() {
+  if (this->reset_pin_ != nullptr) {
+    this->reset_pin_->digital_write(false);
+    delay(reset_duration_);  // NOLINT
+    this->reset_pin_->digital_write(true);
+    delay(reset_duration_);  // NOLINT
+  }
+}
+
+void GDEY042T81::init_display_() {
+  this->reset_();
+
+  this->wait_until_idle_();
+  this->command(0x12);  // SWRESET
+  this->wait_until_idle_();
+
+  // Specify number of lines for the driver: 300 (MUX 300)
+  // https://v4.cecdn.yun300.cn/100001_1909185148/SSD1683.PDF (section 8.1)
+  // https://github.com/ZinggJM/GxEPD2/blob/03d8e7a533c1493f762e392ead12f1bcb7fab8f9/src/gdey/GxEPD2_420_GDEY042T81.cpp#L354
+  this->command(0x01);  //  driver output control
+  this->data(0x2B);     // (height - 1) % 256
+  this->data(0x01);     // (height - 1) / 256
+  this->data(0x00);
+
+  // https://github.com/ZinggJM/GxEPD2/blob/03d8e7a533c1493f762e392ead12f1bcb7fab8f9/src/gdey/GxEPD2_420_GDEY042T81.cpp#L360
+  this->command(0x3C);  // BorderWaveform
+  this->data(0x01);
+  this->command(0x18);  // Read built-in temperature sensor
+  this->data(0x80);
+
+  // GD sample code (Display_EPD_W21.cpp@90ff)
+  this->command(0x11);  // data entry mode
+  this->data(0x03);
+  // set windows (0,0,400,300)
+  this->command(0x44);  // set Ram-X address start/end position
+  this->data(0);
+  this->data(0x31);  // (width / 8 -1)
+
+  this->command(0x45);  //  set Ram-y address start/end position
+  this->data(0);
+  this->data(0);
+  this->data(0x2B);  // (height - 1) % 256
+  this->data(0x01);  // (height - 1) / 256
+
+  // set cursor (0,0)
+  this->command(0x4E);  // set RAM x address count to 0;
+  this->data(0);
+  this->command(0x4F);  // set RAM y address count to 0;
+  this->data(0);
+  this->data(0);
+
+  this->wait_until_idle_();
+}
+
+// https://github.com/ZinggJM/GxEPD2/blob/03d8e7a533c1493f762e392ead12f1bcb7fab8f9/src/gdey/GxEPD2_420_GDEY042T81.cpp#L366
+void GDEY042T81::update_full_() {
+  this->command(0x21);  // display update control
+  this->data(0x40);     // bypass RED as 0
+  this->data(0x00);     // single chip application
+
+  // only ever do a fast update because slow updates are only relevant
+  // for lower operating temperatures
+  // see
+  // https://github.com/ZinggJM/GxEPD2/blob/03d8e7a533c1493f762e392ead12f1bcb7fab8f9/src/gdey/GxEPD2_290_GDEY029T94.h#L30
+  //
+  // Should slow/fast updates be made configurable similar to how GxEPD2 does it? No idea if anyone would need it...
+  this->command(0x1A);  // Write to temperature register
+  this->data(0x6E);
+  this->command(0x22);
+  this->data(0xd7);
+
+  this->command(0x20);
+  this->wait_until_idle_();
+}
+
+// https://github.com/ZinggJM/GxEPD2/blob/03d8e7a533c1493f762e392ead12f1bcb7fab8f9/src/gdey/GxEPD2_420_GDEY042T81.cpp#L389
+void GDEY042T81::update_part_() {
+  this->command(0x21);  // display update control
+  this->data(0x00);     // RED normal
+  this->data(0x00);     // single chip application
+
+  this->command(0x22);
+  this->data(0xfc);
+
+  this->command(0x20);
+  this->wait_until_idle_();
+}
+
+void HOT GDEY042T81::display() {
+  ESP_LOGD(TAG, "Wake up the display");
+  this->init_display_();
+
+  if (!this->wait_until_idle_()) {
+    this->status_set_warning();
+    ESP_LOGE(TAG, "Failed to perform update, display is busy");
+    return;
+  }
+
+  // basic code structure copied from WaveshareEPaper2P9InV2R2
+  if (this->full_update_every_ == 1) {
+    ESP_LOGD(TAG, "Full update");
+    // do single full update
+    this->command(0x24);
+    this->start_data_();
+    this->write_array(this->buffer_, this->get_buffer_length_());
+    this->end_data_();
+
+    // TurnOnDisplay
+    this->update_full_();
+    return;
+  }
+
+  // if (this->full_update_every_ == 1 ||
+  if (this->at_update_ == 0) {
+    ESP_LOGD(TAG, "Update");
+    // do base update
+    this->command(0x24);
+    this->start_data_();
+    this->write_array(this->buffer_, this->get_buffer_length_());
+    this->end_data_();
+
+    this->command(0x26);
+    this->start_data_();
+    this->write_array(this->buffer_, this->get_buffer_length_());
+    this->end_data_();
+
+    // TurnOnDisplay;
+    this->update_full_();
+  } else {
+    // do partial update (full screen)
+    // no need to load a LUT for GoodDisplays as they seem to have the LUT onboard
+    // GD example code (Display_EPD_W21.cpp@283ff)
+    //
+    // not setting the BorderWaveform here again (contrary to the GD example) because according to
+    // https://github.com/ZinggJM/GxEPD2/blob/03d8e7a533c1493f762e392ead12f1bcb7fab8f9/src/gdey/GxEPD2_420_GDEY042T81.cpp#L358
+    // it seems to be enough to set it during display initialization
+    ESP_LOGD(TAG, "Partial update");
+    this->reset_();
+    if (!this->wait_until_idle_()) {
+      this->status_set_warning();
+      ESP_LOGE(TAG, "Failed to perform partial update, display is busy");
+      return;
+    }
+
+    this->command(0x24);
+    this->start_data_();
+    this->write_array(this->buffer_, this->get_buffer_length_());
+    this->end_data_();
+
+    // TurnOnDisplay
+    this->update_part_();
+  }
+
+  this->at_update_ = (this->at_update_ + 1) % this->full_update_every_;
+  this->wait_until_idle_();
+  ESP_LOGD(TAG, "Set the display back to deep sleep");
+  this->deep_sleep();
+}
+void GDEY042T81::set_full_update_every(uint32_t full_update_every) { this->full_update_every_ = full_update_every; }
+int GDEY042T81::get_width_internal() { return 400; }
+int GDEY042T81::get_height_internal() { return 300; }
+uint32_t GDEY042T81::idle_timeout_() { return 5000; }
+void GDEY042T81::dump_config() {
+  LOG_DISPLAY("", "GoodDisplay E-Paper", this);
+  ESP_LOGCONFIG(TAG, "  Model: 4.2in B/W GDEY042T81");
+  ESP_LOGCONFIG(TAG, "  Full Update Every: %" PRIu32, this->full_update_every_);
+  LOG_PIN("  Reset Pin: ", this->reset_pin_);
+  LOG_PIN("  DC Pin: ", this->dc_pin_);
+  LOG_PIN("  Busy Pin: ", this->busy_pin_);
+  LOG_UPDATE_INTERVAL(this);
+}
+
 static const uint8_t LUT_VCOM_DC_4_2[] = {
     0x00, 0x17, 0x00, 0x00, 0x00, 0x02, 0x00, 0x17, 0x17, 0x00, 0x00, 0x02, 0x00, 0x0A, 0x01,
     0x00, 0x00, 0x01, 0x00, 0x0E, 0x0E, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h
index 54e7619ebc..1e7cb6c6c7 100644
--- a/esphome/components/waveshare_epaper/waveshare_epaper.h
+++ b/esphome/components/waveshare_epaper/waveshare_epaper.h
@@ -466,6 +466,43 @@ class WaveshareEPaper2P9InD : public WaveshareEPaper {
   int get_height_internal() override;
 };
 
+class GDEY042T81 : public WaveshareEPaper {
+ public:
+  GDEY042T81();
+
+  void initialize() override;
+
+  void display() override;
+
+  void dump_config() override;
+
+  void deep_sleep() override {
+    // COMMAND POWER OFF
+    this->command(0x22);
+    this->data(0x83);
+    this->command(0x20);
+    // COMMAND DEEP SLEEP
+    this->command(0x10);
+    this->data(0x01);
+  }
+
+  void set_full_update_every(uint32_t full_update_every);
+
+ protected:
+  uint32_t full_update_every_{30};
+  uint32_t at_update_{0};
+
+  int get_width_internal() override;
+  int get_height_internal() override;
+  uint32_t idle_timeout_() override;
+
+ private:
+  void reset_();
+  void update_full_();
+  void update_part_();
+  void init_display_();
+};
+
 class WaveshareEPaper4P2In : public WaveshareEPaper {
  public:
   void initialize() override;
diff --git a/tests/components/waveshare_epaper/common.yaml b/tests/components/waveshare_epaper/common.yaml
index 5889b4659e..09ba1af778 100644
--- a/tests/components/waveshare_epaper/common.yaml
+++ b/tests/components/waveshare_epaper/common.yaml
@@ -463,6 +463,26 @@ display:
     lambda: |-
       it.rectangle(0, 0, it.get_width(), it.get_height());
 
+  - platform: waveshare_epaper
+    id: epd_gdew042t81
+    model: gdey042t81
+    spi_id: spi_waveshare_epaper
+    cs_pin:
+      allow_other_uses: true
+      number: ${cs_pin}
+    dc_pin:
+      allow_other_uses: true
+      number: ${dc_pin}
+    busy_pin:
+      allow_other_uses: true
+      number: ${busy_pin}
+    reset_pin:
+      allow_other_uses: true
+      number: ${reset_pin}
+    full_update_every: 30
+    lambda: |-
+      it.rectangle(0, 0, it.get_width(), it.get_height());
+
   # 4.2 inch displays
   - platform: waveshare_epaper
     id: epd_4_20