#pragma once // Minimal Arduino.h substitute for compiling GxEPD2 under ESP-IDF. GxEPD2's // vendored source calls down into a small, fixed set of Arduino primitives // (digitalWrite/pinMode/digitalRead on plain int16_t pin numbers, plus // delay/micros/yield) - this shim backs them with esphome::GPIOPin via // PinRegistry, so GxEPD2 compiles completely unmodified against it. #include #include #include #include #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "esphome/components/gxepd2/pin_registry.h" #ifndef ESP32 #define ESP32 #endif #define HIGH 1 #define LOW 0 #define INPUT 0 #define OUTPUT 1 #define INPUT_PULLUP 2 inline void pinMode(int16_t pin, uint8_t mode) {} inline void digitalWrite(int16_t pin, uint8_t value) { if (auto *p = esphome::gxepd2::PinRegistry::get(pin)) p->digital_write(value != 0); } inline int digitalRead(int16_t pin) { auto *p = esphome::gxepd2::PinRegistry::get(pin); return (p != nullptr && p->digital_read()) ? HIGH : LOW; } // GxEPD2_EPD::_waitWhileBusy() spins on these two (delay() in its main // branch, yield() right after) for as long as the busy pin stays asserted - // deliberately up to tens of seconds (_busy_timeout), since a real 3/4-color // refresh can legitimately take that long. Neither esphome::delay() nor // esphome::yield() resets the ESP-IDF task watchdog on their own, so // without an explicit reset here, a real (or a misconfigured-polarity, thus // permanently "busy") wait outlasts the watchdog and the whole device // reboots mid-refresh instead of the loop's own timeout ever getting a // chance to fire. // // This calls esp_task_wdt_reset() directly, not esphome::App.feed_wdt(): // feed_wdt()'s rate-limiting state is global, shared by every caller, on // the assumption only the main loop task ever calls it. This code runs on // gxepd2's own task (see gxepd2_display.h), so the main loop's frequent // feed_wdt() calls kept satisfying that shared rate limit and silently // swallowing this task's - esp_task_wdt_reset() always resets whichever // task is actually calling it, isn't rate-limited, and is cheap enough to // call unconditionally here. inline void delay(uint32_t ms) { esp_task_wdt_reset(); esphome::delay(ms); } inline uint32_t micros() { return esphome::micros(); } inline void yield() { esp_task_wdt_reset(); esphome::yield(); } #define pgm_read_byte(addr) (*reinterpret_cast(addr)) // GxEPD2's per-transfer timing diagnostics are opt-in (this component never // enables them, by passing serial_diag_bitrate=0 to init()) and stay // discarded here. But a handful of unconditional warnings - notably // GxEPD2_EPD::_waitWhileBusy()'s "Busy Timeout!" - also go through Serial, // and those matter: they're the only signal that the busy pin never // released. Buffer print() calls and flush to the logger on println(), so // that warning actually surfaces instead of vanishing silently. class GxEPD2SerialShim { public: void begin(uint32_t baud) {} template void print(T value) { this->buffer_ << value; } template void println(T value) { this->buffer_ << value; this->flush_(); } void println() { this->flush_(); } private: void flush_() { std::string line = this->buffer_.str(); if (!line.empty()) ESP_LOGW("gxepd2", "%s", line.c_str()); this->buffer_.str(""); this->buffer_.clear(); } std::ostringstream buffer_; }; inline GxEPD2SerialShim Serial; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)