Browse Source

Add GDEP073E01 (7.3in Spectra6 800x480) panel descriptor

Captured via tools/capture.py from the vendored GxEPD2_730c_GDEP073E01
driver, structurally matched against a fresh emit (check_descriptors.py)
and the standard window-encoder sweep. First panel on the engine's
4bpp/7-color draw_packed_ path.

Two surprises this panel's descriptor hand-fills beyond the emitter's
default output, both backed by driving the real driver rather than
reading its source by hand:
- Non-standard 7-color code map (driver remaps the wire code after
  GxEPD2_7C's standard palette via _convert_to_native) - captured as its
  own PANEL_730C_GDEP073E01_PALETTE, not GXEPD2_PALETTE_7C.
- Asymmetric window-end encoding on 0x83: X end is inclusive (x+w-1),
  Y end is exclusive (y+h) - confirmed across a 5-point window sweep,
  not just the full-frame capture.

Status: structural only, not yet bench-verified.
ZiB 3 weeks ago
parent
commit
d647f286b6

+ 2 - 0
CONTRIBUTING.md

@@ -191,6 +191,7 @@ Derived by the sweep (screen coords `x,y,w,h`):
 | JD79667 | 290c_GDEY029F51 | `0x83=[x,x+w-1,y,y+h-1]` raw-pixel 16-bit BE + mode byte |
 | JD79667 | 290c_GDEY029F51 | `0x83=[x,x+w-1,y,y+h-1]` raw-pixel 16-bit BE + mode byte |
 | JD79676 | 213c_GDEY0213F52 | none — OTP-default frame |
 | JD79676 | 213c_GDEY0213F52 | none — OTP-default frame |
 | HX8717 dual | 579c_GDEY0579F51 | per-region fixed full-frame `0x83` (sweep shows the rect is ignored) |
 | HX8717 dual | 579c_GDEY0579F51 | per-region fixed full-frame `0x83` (sweep shows the rect is ignored) |
+| unknown (GDEP073E01) | 730c_GDEP073E01 | `0x83=[x,x+w-1,y,y+h]` raw-pixel 16-bit BE + flag — **asymmetric end**: X is inclusive (`x+w-1`) but Y is exclusive (`y+h`), one more than JD79667's otherwise-identical `0x83` layout |
 
 
 ## Supported panels
 ## Supported panels
 
 
@@ -206,6 +207,7 @@ from it, and a non-verified panel logs a warning in its boot `dump_config`.
 | `GxEPD2_213c_GDEY0213F52` | GDEY0213F52 | JD79676 | 4-color | ✅ verified |
 | `GxEPD2_213c_GDEY0213F52` | GDEY0213F52 | JD79676 | 4-color | ✅ verified |
 | `GxEPD2_426_GDEQ0426T82` | GDEQ0426T82 | SSD1677 | mono | ✅ verified |
 | `GxEPD2_426_GDEQ0426T82` | GDEQ0426T82 | SSD1677 | mono | ✅ verified |
 | `GxEPD2_583_GDEQ0583T31` | GDEQ0583T31 | UC8179 | mono | ✅ verified |
 | `GxEPD2_583_GDEQ0583T31` | GDEQ0583T31 | UC8179 | mono | ✅ verified |
+| `GxEPD2_730c_GDEP073E01` | GDEP073E01 | unknown | 7-color | ⚪ structural |
 | `GxEPD2_750_GDEY075T7` | GDEY075T7 | UC8179 | mono | ✅ verified |
 | `GxEPD2_750_GDEY075T7` | GDEY075T7 | UC8179 | mono | ✅ verified |
 | `GxEPD2_290c_GDEY029F51` | GDEY029F51 | JD79667 | 4-color | ⚪ structural |
 | `GxEPD2_290c_GDEY029F51` | GDEY029F51 | JD79667 | 4-color | ⚪ structural |
 | `GxEPD2_579c_GDEY0579F51` | GDEY0579F51 | HX8717 | 4-color | ✅ verified |
 | `GxEPD2_579c_GDEY0579F51` | GDEY0579F51 | HX8717 | 4-color | ✅ verified |

+ 1 - 0
components/gxepd2/display.py

@@ -65,6 +65,7 @@ PANELS = {
     "GxEPD2_213c_GDEY0213F52": Panel("PANEL_213C_GDEY0213F52", "GDEY0213F52", "JD79676", "4-color", "verified"),
     "GxEPD2_213c_GDEY0213F52": Panel("PANEL_213C_GDEY0213F52", "GDEY0213F52", "JD79676", "4-color", "verified"),
     "GxEPD2_426_GDEQ0426T82": Panel("PANEL_426_GDEQ0426T82", "GDEQ0426T82", "SSD1677", "mono", "verified"),
     "GxEPD2_426_GDEQ0426T82": Panel("PANEL_426_GDEQ0426T82", "GDEQ0426T82", "SSD1677", "mono", "verified"),
     "GxEPD2_583_GDEQ0583T31": Panel("PANEL_583_GDEQ0583T31", "GDEQ0583T31", "UC8179", "mono", "verified"),
     "GxEPD2_583_GDEQ0583T31": Panel("PANEL_583_GDEQ0583T31", "GDEQ0583T31", "UC8179", "mono", "verified"),
+    "GxEPD2_730c_GDEP073E01": Panel("PANEL_730C_GDEP073E01", "GDEP073E01", "unknown", "7-color", "structural"),
     "GxEPD2_750_GDEY075T7": Panel("PANEL_750_GDEY075T7", "GDEY075T7", "UC8179", "mono", "verified"),
     "GxEPD2_750_GDEY075T7": Panel("PANEL_750_GDEY075T7", "GDEY075T7", "UC8179", "mono", "verified"),
     "GxEPD2_290c_GDEY029F51": Panel("PANEL_290C_GDEY029F51", "GDEY029F51", "JD79667", "4-color", "structural"),
     "GxEPD2_290c_GDEY029F51": Panel("PANEL_290C_GDEY029F51", "GDEY029F51", "JD79667", "4-color", "structural"),
     "GxEPD2_579c_GDEY0579F51": Panel("PANEL_579C_GDEY0579F51", "GDEY0579F51", "HX8717", "4-color", "verified"),
     "GxEPD2_579c_GDEY0579F51": Panel("PANEL_579C_GDEY0579F51", "GDEY0579F51", "HX8717", "4-color", "verified"),

+ 1 - 0
components/gxepd2/gxepd2_display.h

@@ -14,6 +14,7 @@
 #include "panel_579c_gdey0579f51.h"
 #include "panel_579c_gdey0579f51.h"
 #include "panel_579c_gdey0579f51_master_only.h"
 #include "panel_579c_gdey0579f51_master_only.h"
 #include "panel_583_gdeq0583t31.h"
 #include "panel_583_gdeq0583t31.h"
+#include "panel_730c_gdep073e01.h"
 #include "panel_750_gdey075t7.h"
 #include "panel_750_gdey075t7.h"
 
 
 namespace esphome::gxepd2 {
 namespace esphome::gxepd2 {

+ 138 - 0
components/gxepd2/panel_730c_gdep073e01.h

@@ -0,0 +1,138 @@
+#pragma once
+
+// Captured via tools/capture.py from
+// third_party/GxEPD2/src/epd7c/GxEPD2_730c_GDEP073E01.{h,cpp} (GDEP073E01,
+// controller unknown per the vendor header comment, 800x480, 7-color
+// Spectra6). Init/window/refresh command sequences are observed byte-for-
+// byte by the emitter, not hand-transcribed.
+//
+// FIRST panel on this engine's 4bpp/7-color path (bits_per_pixel=4,
+// GXEPD2_PALETTE_7C was scaffolded but never consumed before this).
+//
+// Two surprises the emitter flagged / the window sweep confirmed, both
+// genuinely new vs. the other 4 window-encoder families already seen
+// (gxepd2-window-encoders memory):
+//
+// 1. Non-standard 7-color code map: this driver's own
+//    GxEPD2_730c_GDEP073E01::_convert_to_native() remaps the wire code
+//    AFTER the vendor's GxEPD2_7C wrapper produces the standard
+//    black/white/green/blue/red/yellow/orange = 0..6 codes. Driving the
+//    real wrapper (emit_palette_7c.h) and reading the wire byte - not
+//    hand-reading the switch statement - gave the true native codes:
+//    black=0x00, white=0x01, green=0x06, blue=0x05, red=0x03, yellow=0x02,
+//    orange=0x04 (white stays 0x01, only the other five rotate). Encoded
+//    below as its own palette table rather than GXEPD2_PALETTE_7C.
+// 2. Asymmetric window-end encoding on its single 0x83 command: X is
+//    inclusive-end (xe = x+w-1) but Y is exclusive-end (ye = y+h, one
+//    more than X's convention) - confirmed by sweeping 5 distinct rects
+//    (tools/gxepd2_harness/run_730c_gdep073e01.cpp) and seeing ye = y+h
+//    hold at every point, including non-8-aligned y. The driver's own
+//    comment on _setPartialRamArea calls this out: "controller issue (one
+//    more)". Captured here as plain data bytes, so the asymmetry needs no
+//    special engine support - just don't hand-rederive this window math
+//    for a sibling panel and assume symmetric inclusive ends.
+//
+// Single plane, single region, plain row-major writeNative addressing (no
+// row_interleave, no window_per_plane) - structurally the simplest shape
+// once the above two facts are captured correctly.
+//
+// STATUS: structural only - not yet bench-verified (see display.py).
+
+#include "epd_command.h"
+
+namespace esphome::gxepd2 {
+
+inline const uint8_t PANEL_730C_GDEP073E01_CMDH[] = {0x49, 0x55, 0x20, 0x08, 0x09, 0x18};
+inline const uint8_t PANEL_730C_GDEP073E01_PWRR[] = {0x3F};
+inline const uint8_t PANEL_730C_GDEP073E01_PSR[] = {0x5F, 0x69};
+inline const uint8_t PANEL_730C_GDEP073E01_POFS[] = {0x00, 0x54, 0x00, 0x44};
+inline const uint8_t PANEL_730C_GDEP073E01_BTST1[] = {0x40, 0x1F, 0x1F, 0x2C};
+inline const uint8_t PANEL_730C_GDEP073E01_BTST2[] = {0x6F, 0x1F, 0x17, 0x49};
+inline const uint8_t PANEL_730C_GDEP073E01_BTST3[] = {0x6F, 0x1F, 0x1F, 0x22};
+inline const uint8_t PANEL_730C_GDEP073E01_PLL[] = {0x08};
+inline const uint8_t PANEL_730C_GDEP073E01_CDI[] = {0x3F};
+inline const uint8_t PANEL_730C_GDEP073E01_TCON[] = {0x02, 0x00};
+inline const uint8_t PANEL_730C_GDEP073E01_TRES[] = {0x03, 0x20, 0x01, 0xE0};  // WIDTH=800, HEIGHT=480
+inline const uint8_t PANEL_730C_GDEP073E01_T_VDCS[] = {0x01};
+inline const uint8_t PANEL_730C_GDEP073E01_PWS[] = {0x2F};
+
+inline const EpdCommand PANEL_730C_GDEP073E01_INIT[] = {
+    {0xAA, PANEL_730C_GDEP073E01_CMDH, 6},
+    {0x01, PANEL_730C_GDEP073E01_PWRR, 1},
+    {0x00, PANEL_730C_GDEP073E01_PSR, 2},
+    {0x03, PANEL_730C_GDEP073E01_POFS, 4},
+    {0x05, PANEL_730C_GDEP073E01_BTST1, 4},
+    {0x06, PANEL_730C_GDEP073E01_BTST2, 4},
+    {0x08, PANEL_730C_GDEP073E01_BTST3, 4},
+    {0x30, PANEL_730C_GDEP073E01_PLL, 1},
+    {0x50, PANEL_730C_GDEP073E01_CDI, 1},
+    {0x60, PANEL_730C_GDEP073E01_TCON, 2},
+    {0x61, PANEL_730C_GDEP073E01_TRES, 4},
+    {0x84, PANEL_730C_GDEP073E01_T_VDCS, 1},
+    {0xE3, PANEL_730C_GDEP073E01_PWS, 1},
+    {0x04, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},  // power on
+};
+
+// _setPartialRamArea(0, 0, 800, 480): xe = x+w-1 = 799 (inclusive), but
+// ye = y+h = 480 (exclusive - see the file header note). Confirmed by the
+// 5-point window sweep, not just this one full-frame capture.
+inline const uint8_t PANEL_730C_GDEP073E01_WINDOW[] = {
+    0x00, 0x00,  // x/256, x%256   = 0
+    0x03, 0x1F,  // xe/256, xe%256 = 799
+    0x00, 0x00,  // y/256, y%256   = 0
+    0x01, 0xE0,  // ye/256, ye%256 = 480
+    0x01,
+};
+
+inline const EpdCommand PANEL_730C_GDEP073E01_WINDOW_SEQ[] = {
+    {0x83, PANEL_730C_GDEP073E01_WINDOW, 9},
+};
+
+// _refresh(false): 0x50=0x3F, then 0x12+delay(1)+_waitWhileBusy - collapsed
+// into one wait_busy_after.
+inline const uint8_t PANEL_730C_GDEP073E01_CDI_REFRESH[] = {0x3F};
+inline const uint8_t PANEL_730C_GDEP073E01_REFRESH_TRIGGER[] = {0x00};
+
+inline const EpdCommand PANEL_730C_GDEP073E01_REFRESH[] = {
+    {0x50, PANEL_730C_GDEP073E01_CDI_REFRESH, 1},
+    {0x12, PANEL_730C_GDEP073E01_REFRESH_TRIGGER, 1, /*delay_before_ms=*/0, /*delay_after_ms=*/0,
+     /*wait_busy_after=*/true},
+};
+
+// NOT GXEPD2_PALETTE_7C - see the file header. Codes observed by driving
+// the vendor's own GxEPD2_7C wrapper and reading the wire byte
+// (emit_palette_7c.h), then cross-checked against
+// GxEPD2_730c_GDEP073E01::_convert_to_native() in the driver source.
+inline const EpdPaletteEntry PANEL_730C_GDEP073E01_PALETTE[] = {
+    {0x00, 0x00, 0x00, 0x00},  // black
+    {0xFF, 0xFF, 0xFF, 0x01},  // white
+    {0x00, 0xFF, 0x00, 0x06},  // green
+    {0x00, 0x00, 0xFF, 0x05},  // blue
+    {0xFF, 0x00, 0x00, 0x03},  // red
+    {0xFF, 0xFF, 0x00, 0x02},  // yellow
+    {0xFF, 0xA4, 0x00, 0x04},  // orange
+};
+inline const uint8_t PANEL_730C_GDEP073E01_PALETTE_LEN =
+    sizeof(PANEL_730C_GDEP073E01_PALETTE) / sizeof(PANEL_730C_GDEP073E01_PALETTE[0]);
+
+inline const EpdPanelDescriptor PANEL_730C_GDEP073E01 = {
+    .width = 800,
+    .height = 480,
+    .busy_active_level = false,  // GxEPD2_EPD(..., /*busy_level=*/LOW, ...)
+    .busy_timeout_ms = 60000,    // full_refresh_time is 15000ms; generous diagnostic ceiling
+    .init_sequence = PANEL_730C_GDEP073E01_INIT,
+    .init_sequence_len = sizeof(PANEL_730C_GDEP073E01_INIT) / sizeof(PANEL_730C_GDEP073E01_INIT[0]),
+    .window_sequence = {PANEL_730C_GDEP073E01_WINDOW_SEQ, nullptr},
+    .window_sequence_len = {sizeof(PANEL_730C_GDEP073E01_WINDOW_SEQ) / sizeof(PANEL_730C_GDEP073E01_WINDOW_SEQ[0]), 0},
+    .plane_count = 1,
+    .plane_write_cmd = {0x10, 0x00},
+    .plane_invert = {false, false},
+    .bits_per_pixel = 4,
+    .palette = PANEL_730C_GDEP073E01_PALETTE,
+    .palette_len = PANEL_730C_GDEP073E01_PALETTE_LEN,
+    .blank_code = 0x01,
+    .refresh_sequence = PANEL_730C_GDEP073E01_REFRESH,
+    .refresh_sequence_len = sizeof(PANEL_730C_GDEP073E01_REFRESH) / sizeof(PANEL_730C_GDEP073E01_REFRESH[0]),
+};
+
+}  // namespace esphome::gxepd2

+ 36 - 0
tests/test_pattern_730c_gdep073e01.esp32-idf.yaml

@@ -0,0 +1,36 @@
+# Bench visual-check pattern for the new GDEP073E01 (7.3" Spectra6, 800x480,
+# 7-color) panel - see components/gxepd2/panel_730c_gdep073e01.h for the
+# capture notes (non-standard color codes, asymmetric window-end encoding).
+# FIRST panel to exercise the engine's 4bpp/7-color draw_packed_ path.
+# Copy of test_pattern_bar_ladder_diagonal.esp32-idf.yaml's lambda, just
+# retargeted via model:.
+esphome:
+  name: gxepd2-test-pattern-730c
+esp32:
+  board: esp32dev
+  framework:
+    type: esp-idf
+external_components:
+  - source:
+      type: local
+      path: ../components
+    components: [gxepd2]
+spi:
+  clk_pin: 18
+  mosi_pin: 23
+logger:
+font:
+  - file: "gfonts://Roboto"
+    id: panel_label_font
+    size: 24
+display:
+  - platform: gxepd2
+    id: gxepd2_test
+    model: GxEPD2_730c_GDEP073E01
+    cs_pin: 5
+    dc_pin: 17
+    reset_pin: 16
+    busy_pin: 4
+    update_interval: 60s
+    lambda: |-
+      id(gxepd2_test).test_pattern(id(panel_label_font));

+ 8 - 0
tools/gxepd2_harness/run_730c_gdep073e01.cpp

@@ -0,0 +1,8 @@
+#include "run_harness.h"
+
+#include "epd7c/GxEPD2_730c_GDEP073E01.h"
+
+int main() {
+  run_harness<GxEPD2_730c_GDEP073E01>("GxEPD2_730c_GDEP073E01");
+  return 0;
+}