Browse Source

Fix "Attempted read from write-only channel" bricking every panel command

arduino_compat/SPI.h's transfer() called esphome's transfer_byte(), which
requests a real bidirectional exchange. E-paper wiring normally leaves
MISO unconnected, so esphome's spi: bus auto-registers the device as
write-only - and a write-only ESP-IDF SPI device doesn't just skip the
read half of a transfer() call, it drops the write too (see
SPIDelegateHw::transfer in esphome's spi_esp_idf.cpp), silently
discarding every command and data byte GxEPD2 ever sent.

Switch to write_byte(), which only ever asks to write and works
regardless of whether MISO is wired. The only GxEPD2 panels that
actually read a response (IT8951 host-controller boards) can no longer
be supported through this shim, so exclude them from the model list
instead of leaving a selectable model that was already silently
non-functional.

Verified against the native esp-idf and PlatformIO toolchains.
ZiB 1 month ago
parent
commit
ff647b2252
2 changed files with 21 additions and 2 deletions
  1. 13 1
      components/gxepd2/arduino_compat/SPI.h
  2. 8 1
      components/gxepd2/display.py

+ 13 - 1
components/gxepd2/arduino_compat/SPI.h

@@ -26,7 +26,19 @@ class SPIClass : public esphome::spi::SPIDevice<esphome::spi::BIT_ORDER_MSB_FIRS
   void end() {}
   void end() {}
   void beginTransaction(SPISettings settings) { this->enable(); }
   void beginTransaction(SPISettings settings) { this->enable(); }
   void endTransaction() { this->disable(); }
   void endTransaction() { this->disable(); }
-  uint8_t transfer(uint8_t data) { return this->transfer_byte(data); }
+  // GxEPD2 panels (all but the IT8951-based ones, which aren't supported -
+  // see display.py) never read anything back over MISO; e-paper wiring
+  // normally leaves it unconnected, and esphome's spi: bus then registers
+  // this device as write-only. transfer_byte()/delegate transfer() ask for
+  // a real bidirectional exchange regardless, which a write-only device
+  // refuses outright - it doesn't just skip the read, it drops the write
+  // too (see SPIDelegateHw::transfer in spi_esp_idf.cpp), silently bricking
+  // every panel command. write_byte() only ever asks to write, so it works
+  // whether or not MISO is wired.
+  uint8_t transfer(uint8_t data) {
+    this->write_byte(data);
+    return 0;
+  }
 };
 };
 
 
 // Unused: every gxepd2 instance overrides this via selectSPI() with its own
 // Unused: every gxepd2 instance overrides this via selectSPI() with its own

+ 8 - 1
components/gxepd2/display.py

@@ -49,7 +49,14 @@ WRAPPER_DEFINES = {
     "GxEPD2_4C": "USE_GXEPD2_4C",
     "GxEPD2_4C": "USE_GXEPD2_4C",
     "GxEPD2_7C": "USE_GXEPD2_7C",
     "GxEPD2_7C": "USE_GXEPD2_7C",
 }
 }
-MODELS = {model_id: entry for model_id, entry in CATALOG.items() if entry["wrapper"] in COLOR_MODES}
+# IT8951 host-controller panels need genuine bidirectional SPI (they read
+# status/data back over MISO); arduino_compat/SPI.h's transfer() is
+# write-only - see the comment there - so these can never work through it.
+MODELS = {
+    model_id: entry
+    for model_id, entry in CATALOG.items()
+    if entry["wrapper"] in COLOR_MODES and not entry["source"].startswith("it8951/")
+}
 
 
 gxepd2_ns = cg.esphome_ns.namespace("gxepd2")
 gxepd2_ns = cg.esphome_ns.namespace("gxepd2")
 GxEPD2Display = gxepd2_ns.class_("GxEPD2Display", display.DisplayBuffer)
 GxEPD2Display = gxepd2_ns.class_("GxEPD2Display", display.DisplayBuffer)