|
@@ -1,302 +1,104 @@
|
|
|
#pragma once
|
|
#pragma once
|
|
|
|
|
|
|
|
-#include <atomic>
|
|
|
|
|
-
|
|
|
|
|
-#include <esp_task_wdt.h>
|
|
|
|
|
-#include <freertos/FreeRTOS.h>
|
|
|
|
|
-#include <freertos/semphr.h>
|
|
|
|
|
-#include <freertos/task.h>
|
|
|
|
|
-
|
|
|
|
|
#include "esphome/core/component.h"
|
|
#include "esphome/core/component.h"
|
|
|
#include "esphome/core/helpers.h"
|
|
#include "esphome/core/helpers.h"
|
|
|
#include "esphome/components/display/display_buffer.h"
|
|
#include "esphome/components/display/display_buffer.h"
|
|
|
#include "esphome/components/spi/spi.h"
|
|
#include "esphome/components/spi/spi.h"
|
|
|
-#include "pin_registry.h"
|
|
|
|
|
-
|
|
|
|
|
-// Only the wrapper the configured model needs gets included - see
|
|
|
|
|
-// display.py's USE_GXEPD2_* defines. Including all four unconditionally
|
|
|
|
|
-// fails to parse under ESP-IDF's toolchain: the un-instantiated template
|
|
|
|
|
-// bodies of the unused ones hit the same bare WIDTH/HEIGHT names, and
|
|
|
|
|
-// GCC's two-phase lookup only resolves those once instantiated.
|
|
|
|
|
-#ifdef USE_GXEPD2_BW
|
|
|
|
|
-#include <GxEPD2_BW.h>
|
|
|
|
|
-#endif
|
|
|
|
|
-#ifdef USE_GXEPD2_3C
|
|
|
|
|
-#include <GxEPD2_3C.h>
|
|
|
|
|
-#endif
|
|
|
|
|
-#ifdef USE_GXEPD2_4C
|
|
|
|
|
-#include <GxEPD2_4C.h>
|
|
|
|
|
-#endif
|
|
|
|
|
-#ifdef USE_GXEPD2_7C
|
|
|
|
|
-#include <GxEPD2_7C.h>
|
|
|
|
|
-#endif
|
|
|
|
|
|
|
+#include "cycle_engine.h"
|
|
|
|
|
+#include "panel_213_z98c.h"
|
|
|
|
|
|
|
|
namespace esphome::gxepd2 {
|
|
namespace esphome::gxepd2 {
|
|
|
|
|
|
|
|
static const char *const TAG = "gxepd2";
|
|
static const char *const TAG = "gxepd2";
|
|
|
|
|
|
|
|
-// Selected by codegen from catalog.json's "wrapper" field.
|
|
|
|
|
-enum class GxEPD2ColorMode {
|
|
|
|
|
- BW,
|
|
|
|
|
- THREE_COLOR, // black/white plus one accent ink (red or yellow panels alike)
|
|
|
|
|
- FOUR_COLOR, // black/white/red/yellow(-ish) 4-ink panels
|
|
|
|
|
- SEVEN_COLOR, // full ACeP-style palette
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-// DriverT: a GxEPD2 panel driver class (e.g. GxEPD2_290_T94).
|
|
|
|
|
-// DisplayT: that driver wrapped in its GxEPD2 color-depth template (e.g.
|
|
|
|
|
-// GxEPD2_BW<GxEPD2_290_T94, GxEPD2_290_T94::HEIGHT>).
|
|
|
|
|
-// DualCS: whether DriverT takes 5 constructor args (cs, dc, rst, busy, cs2)
|
|
|
|
|
-// instead of 4. All four are chosen by codegen from catalog.json.
|
|
|
|
|
-//
|
|
|
|
|
-// cs_pin/dc_pin/reset_pin/busy_pin(/cs2_pin) are synthetic PinRegistry ids,
|
|
|
|
|
-// not real esphome::GPIOPin; digitalWrite()/digitalRead()/pinMode() in
|
|
|
|
|
-// arduino_compat/Arduino.h resolve them back to the real pin.
|
|
|
|
|
-//
|
|
|
|
|
-// display::DisplayBuffer already derives PollingComponent via Display - do
|
|
|
|
|
-// not add PollingComponent again here, it creates an ambiguous base.
|
|
|
|
|
-template<typename DriverT, typename DisplayT, GxEPD2ColorMode ColorMode, bool DualCS>
|
|
|
|
|
|
|
+// Native chunked-engine replacement for the old GxEPD2-template/FreeRTOS-task
|
|
|
|
|
+// component - see the plan this was built from (and project memory entry
|
|
|
|
|
+// gxepd2_task_watchdog_status) for why. Only GxEPD2_213_Z98c is wired up so
|
|
|
|
|
+// far; more panels arrive as hand-written (later, extracted) descriptors,
|
|
|
|
|
+// not by templating this class over GxEPD2's own driver classes.
|
|
|
class GxEPD2Display : public display::DisplayBuffer {
|
|
class GxEPD2Display : public display::DisplayBuffer {
|
|
|
public:
|
|
public:
|
|
|
- GxEPD2Display(int16_t cs_pin, int16_t dc_pin, int16_t reset_pin, int16_t busy_pin)
|
|
|
|
|
- : cs_(cs_pin), dc_(dc_pin), rst_(reset_pin), busy_(busy_pin) {
|
|
|
|
|
- static_assert(!DualCS, "use the 5-pin constructor for a dual-CS model");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- GxEPD2Display(int16_t cs_pin, int16_t dc_pin, int16_t reset_pin, int16_t busy_pin, int16_t cs2_pin)
|
|
|
|
|
- : cs_(cs_pin), dc_(dc_pin), rst_(reset_pin), busy_(busy_pin), cs2_(cs2_pin) {
|
|
|
|
|
- static_assert(DualCS, "the 5-pin constructor is only for a dual-CS model");
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- ~GxEPD2Display() {
|
|
|
|
|
- if (this->update_task_ != nullptr) {
|
|
|
|
|
- vTaskDelete(this->update_task_);
|
|
|
|
|
- }
|
|
|
|
|
- if (this->driver_ != nullptr) {
|
|
|
|
|
- this->driver_->~DisplayT();
|
|
|
|
|
- RAMAllocator<uint8_t>().deallocate(reinterpret_cast<uint8_t *>(this->driver_), 0);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- void set_pin(int16_t id, GPIOPin *pin) { PinRegistry::set(id, pin); }
|
|
|
|
|
|
|
+ void set_cs_pin(GPIOPin *pin) { this->cs_ = pin; }
|
|
|
|
|
+ void set_dc_pin(GPIOPin *pin) { this->dc_ = pin; }
|
|
|
|
|
+ void set_reset_pin(GPIOPin *pin) { this->rst_ = pin; }
|
|
|
|
|
+ void set_busy_pin(GPIOPin *pin) { this->busy_ = pin; }
|
|
|
void set_spi_parent(spi::SPIComponent *parent) { this->spi_.set_spi_parent(parent); }
|
|
void set_spi_parent(spi::SPIComponent *parent) { this->spi_.set_spi_parent(parent); }
|
|
|
|
|
|
|
|
- // DisplayT (holds the page buffer, by far the biggest thing in this
|
|
|
|
|
- // component) is allocated here via RAMAllocator, not as a plain member -
|
|
|
|
|
- // ESPHome's Pvariable placement-news components into static storage, so
|
|
|
|
|
- // this is the one deliberate, explicitly-sized dynamic allocation that
|
|
|
|
|
- // lets the buffer prefer PSRAM when available, made once at boot and
|
|
|
|
|
- // never freed.
|
|
|
|
|
void setup() override {
|
|
void setup() override {
|
|
|
|
|
+ const auto *desc = &PANEL_213_Z98C;
|
|
|
|
|
+ this->plane_bytes_ = (static_cast<uint32_t>(desc->width) * desc->height) / 8;
|
|
|
RAMAllocator<uint8_t> allocator;
|
|
RAMAllocator<uint8_t> allocator;
|
|
|
- void *mem = allocator.allocate(sizeof(DisplayT));
|
|
|
|
|
- if (mem == nullptr) {
|
|
|
|
|
- ESP_LOGE(TAG, "Failed to allocate %u bytes for display buffer", (unsigned) sizeof(DisplayT));
|
|
|
|
|
|
|
+ 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)) {
|
|
|
|
|
+ ESP_LOGE(TAG, "Failed to allocate %u-byte plane buffer(s)", (unsigned) this->plane_bytes_);
|
|
|
this->mark_failed();
|
|
this->mark_failed();
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
- if constexpr (DualCS) {
|
|
|
|
|
- this->driver_ = new (mem) DisplayT(DriverT(this->cs_, this->dc_, this->rst_, this->busy_, this->cs2_));
|
|
|
|
|
- } else {
|
|
|
|
|
- this->driver_ = new (mem) DisplayT(DriverT(this->cs_, this->dc_, this->rst_, this->busy_));
|
|
|
|
|
- }
|
|
|
|
|
- this->spi_.spi_setup();
|
|
|
|
|
- this->driver_->epd2.selectSPI(this->spi_, SPISettings());
|
|
|
|
|
- // GxEPD2's init() does a real panel clear/refresh, blocking on busy just
|
|
|
|
|
- // like a full update does - it runs during ESPHome's synchronous setup()
|
|
|
|
|
- // phase, before any component's dump_config() is called, which is why
|
|
|
|
|
- // the whole boot log can appear to stall here.
|
|
|
|
|
- uint32_t init_start = millis();
|
|
|
|
|
- this->driver_->init(0);
|
|
|
|
|
- ESP_LOGD(TAG, "GxEPD2 init took %u ms", (unsigned) (millis() - init_start));
|
|
|
|
|
|
|
+ memset(this->plane0_, 0xFF, this->plane_bytes_);
|
|
|
|
|
+ if (this->plane1_ != nullptr)
|
|
|
|
|
+ memset(this->plane1_, 0xFF, this->plane_bytes_);
|
|
|
|
|
|
|
|
- // GxEPD2's own refresh cycle - draw callbacks, the SPI write burst, and
|
|
|
|
|
- // above all the busy-pin wait - is synchronous top to bottom, with no
|
|
|
|
|
- // yield points we can hook to turn it into the kind of chunked,
|
|
|
|
|
- // resumable loop() state machine ESPHome components are expected to be
|
|
|
|
|
- // (that's how esphome's own epaper_spi avoids ever blocking: its
|
|
|
|
|
- // update() just arms a flag and returns, and loop() polls busy
|
|
|
|
|
- // non-blockingly on every main-loop tick). Rewriting GxEPD2 itself to
|
|
|
|
|
- // work that way is out of reach here, so instead the whole sequence
|
|
|
|
|
- // runs on its own FreeRTOS task: update() just wakes this task and
|
|
|
|
|
- // returns immediately, so the real several-seconds-long wait happens
|
|
|
|
|
- // off the main loop entirely, and WiFi/API/every other component stay
|
|
|
|
|
- // responsive through a refresh instead of stalling with it.
|
|
|
|
|
- //
|
|
|
|
|
- // Caveats accepted for now, matching this project's actual constraint
|
|
|
|
|
- // (an update() that blocks the whole app is a non-starter, full stop):
|
|
|
|
|
- // - The draw lambda (and anything it reads - sensors, globals, other
|
|
|
|
|
- // components) now runs off the main loop's thread. ESPHome code is
|
|
|
|
|
- // generally written assuming single-threaded access; simple POD
|
|
|
|
|
- // reads (numbers, bools) are practically fine, but this is a real
|
|
|
|
|
- // departure from the norm worth keeping in mind for anything richer.
|
|
|
|
|
- // - A second update() call while one is still running is rejected
|
|
|
|
|
- // outright (logged, not queued) via update_running_ below - no
|
|
|
|
|
- // attempt to serialize or coalesce them.
|
|
|
|
|
- // - The SPI bus is only actually held (bus acquired, CS asserted) for
|
|
|
|
|
- // the write bursts, not for the busy-pin wait in between, so a
|
|
|
|
|
- // shared spi: bus should mostly be fine - but that's about the
|
|
|
|
|
- // write phase, not this task boundary, and hasn't been stress-tested
|
|
|
|
|
- // against another device on the same bus mid-refresh.
|
|
|
|
|
- //
|
|
|
|
|
- // KNOWN ISSUE, unresolved as of this comment: on real hardware,
|
|
|
|
|
- // "gxepd2_update" itself still sometimes trips the ESP-IDF task
|
|
|
|
|
- // watchdog (confirmed via the panic naming it directly), despite: (1)
|
|
|
|
|
- // being explicitly subscribed to it (see update_task_loop_), and (2)
|
|
|
|
|
- // every long-running spot we've found - the busy-wait in
|
|
|
|
|
- // arduino_compat/Arduino.h, the per-256-byte throttle in
|
|
|
|
|
- // arduino_compat/SPI.h - calling esp_task_wdt_reset() directly rather
|
|
|
|
|
- // than through esphome's globally-rate-limited App.feed_wdt(). Not
|
|
|
|
|
- // chased further this session; likely another GxEPD2 call path that
|
|
|
|
|
- // runs long without hitting either of those two spots.
|
|
|
|
|
- //
|
|
|
|
|
- // Bigger-picture option worth weighing before sinking more time into
|
|
|
|
|
- // patching this shape further: the actual value GxEPD2 gives us is its
|
|
|
|
|
- // per-panel database (dimensions, init sequences, LUTs, quirks like
|
|
|
|
|
- // 579c's master/slave split) and a reference implementation to check
|
|
|
|
|
- // against - not really its control flow, which is exactly what's
|
|
|
|
|
- // fighting us here (synchronous, no yield points, not built for
|
|
|
|
|
- // esphome's cooperative loop() model). A future pass could extract
|
|
|
|
|
- // just the per-device data/sequences and reimplement the actual
|
|
|
|
|
- // drawing/refresh loop natively in esphome's chunked loop() style
|
|
|
|
|
- // (like epaper_spi), dropping this task entirely along with whatever
|
|
|
|
|
- // watchdog and thread-safety fragility comes with it. That's a real
|
|
|
|
|
- // rewrite, not a patch - worth a dedicated pass, not a continuation of
|
|
|
|
|
- // this one.
|
|
|
|
|
- this->lambda_done_sem_ = xSemaphoreCreateBinary();
|
|
|
|
|
- xTaskCreate(&GxEPD2Display::update_task_trampoline_, "gxepd2_update", 4096, this, 1, &this->update_task_);
|
|
|
|
|
|
|
+ this->spi_.spi_setup();
|
|
|
|
|
+ this->engine_.begin(desc, this->cs_, this->dc_, this->rst_, this->busy_, &this->spi_);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // Arms the state machine and returns immediately - the real several-
|
|
|
|
|
+ // seconds-long write/refresh/busy-wait cycle happens across many
|
|
|
|
|
+ // subsequent loop() ticks, not before this call returns.
|
|
|
void update() override {
|
|
void update() override {
|
|
|
- bool expected = false;
|
|
|
|
|
- if (!this->update_running_.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) {
|
|
|
|
|
|
|
+ if (!this->engine_.idle()) {
|
|
|
ESP_LOGW(TAG, "Skipping update: previous display refresh is still running");
|
|
ESP_LOGW(TAG, "Skipping update: previous display refresh is still running");
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
- // update_task_loop_() suspends this exact task around every later
|
|
|
|
|
- // page's draw lambda too (see there) - it needs to know who to
|
|
|
|
|
- // suspend, since after this call returns nothing keeps it waiting for
|
|
|
|
|
- // update_task_ to signal it.
|
|
|
|
|
- this->caller_task_ = xTaskGetCurrentTaskHandle();
|
|
|
|
|
- xTaskNotifyGive(this->update_task_);
|
|
|
|
|
- // Block only through this frame's draw lambda - not through the SPI
|
|
|
|
|
- // write / physical refresh that follows, which continues on
|
|
|
|
|
- // update_task_ after we return. For a paged (height_divisor > 1) model
|
|
|
|
|
- // this covers only the first page's lambda call synchronously; later
|
|
|
|
|
- // pages' lambda calls happen after we've returned, protected instead
|
|
|
|
|
- // by briefly suspending this task around each one (below).
|
|
|
|
|
- xSemaphoreTake(this->lambda_done_sem_, portMAX_DELAY);
|
|
|
|
|
|
|
+ this->do_update_();
|
|
|
|
|
+ this->engine_.request_full_update(this->plane0_, this->plane1_);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ void loop() override { this->engine_.step(); }
|
|
|
|
|
+
|
|
|
void dump_config() override {
|
|
void dump_config() override {
|
|
|
ESP_LOGCONFIG(TAG, "GxEPD2 Display");
|
|
ESP_LOGCONFIG(TAG, "GxEPD2 Display");
|
|
|
LOG_UPDATE_INTERVAL(this);
|
|
LOG_UPDATE_INTERVAL(this);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- display::DisplayType get_display_type() override {
|
|
|
|
|
- if constexpr (ColorMode == GxEPD2ColorMode::BW) {
|
|
|
|
|
- return display::DisplayType::DISPLAY_TYPE_BINARY;
|
|
|
|
|
- } else {
|
|
|
|
|
- return display::DisplayType::DISPLAY_TYPE_COLOR;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
|
|
|
|
|
|
|
protected:
|
|
protected:
|
|
|
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 constexpr (ColorMode == GxEPD2ColorMode::BW) {
|
|
|
|
|
- this->driver_->drawPixel(x, y, color.is_on() ? GxEPD_BLACK : GxEPD_WHITE);
|
|
|
|
|
- } else if constexpr (ColorMode == GxEPD2ColorMode::THREE_COLOR) {
|
|
|
|
|
- if (color.red > color.green && color.red > color.blue && color.red > 0x7f) {
|
|
|
|
|
- this->driver_->drawPixel(x, y, GxEPD_RED);
|
|
|
|
|
|
|
+ if (x < 0 || y < 0 || x >= PANEL_213_Z98C.width || y >= PANEL_213_Z98C.height)
|
|
|
|
|
+ return;
|
|
|
|
|
+ uint32_t idx = (static_cast<uint32_t>(y) * PANEL_213_Z98C.width + x) / 8;
|
|
|
|
|
+ uint8_t mask = 0x80 >> (x % 8);
|
|
|
|
|
+ // Same classification the old task-based component used: red/accent
|
|
|
|
|
+ // pixels go to the color plane, everything else is black/white on the
|
|
|
|
|
+ // black plane. get_display_type() reports DISPLAY_TYPE_COLOR, so
|
|
|
|
|
+ // callers draw literal RGB colors, not the on/off is_on() convention.
|
|
|
|
|
+ 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_[idx] &= ~mask; // 0 bit = colored, per GxEPD2_213_Z98c::writeImage's color plane
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this->plane1_[idx] |= mask;
|
|
|
|
|
+ if (bright) {
|
|
|
|
|
+ this->plane0_[idx] |= mask;
|
|
|
} else {
|
|
} else {
|
|
|
- // get_display_type() reports DISPLAY_TYPE_COLOR for this mode, so
|
|
|
|
|
- // callers draw with literal RGB colors (Color(255,255,255,...) for
|
|
|
|
|
- // white, Color(0,0,0,...) for black) - not the on/off convention
|
|
|
|
|
- // is_on() (raw_32 != 0) assumes, which is only correct for the BW
|
|
|
|
|
- // path above (DISPLAY_TYPE_BINARY). Using is_on() here inverted
|
|
|
|
|
- // black and white for any lambda drawing literal colors.
|
|
|
|
|
- bool bright = color.red > 0x7f || color.green > 0x7f || color.blue > 0x7f;
|
|
|
|
|
- this->driver_->drawPixel(x, y, bright ? GxEPD_WHITE : GxEPD_BLACK);
|
|
|
|
|
|
|
+ this->plane0_[idx] &= ~mask;
|
|
|
}
|
|
}
|
|
|
- } else {
|
|
|
|
|
- uint16_t rgb565 = ((color.red & 0xF8) << 8) | ((color.green & 0xFC) << 3) | (color.blue >> 3);
|
|
|
|
|
- this->driver_->drawPixel(x, y, rgb565);
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- int get_width_internal() override { return this->driver_->width(); }
|
|
|
|
|
- int get_height_internal() override { return this->driver_->height(); }
|
|
|
|
|
-
|
|
|
|
|
- int16_t cs_;
|
|
|
|
|
- int16_t dc_;
|
|
|
|
|
- int16_t rst_;
|
|
|
|
|
- int16_t busy_;
|
|
|
|
|
- int16_t cs2_{-1};
|
|
|
|
|
- DisplayT *driver_{nullptr};
|
|
|
|
|
- SPIClass spi_;
|
|
|
|
|
-
|
|
|
|
|
- private:
|
|
|
|
|
- static void update_task_trampoline_(void *param) { static_cast<GxEPD2Display *>(param)->update_task_loop_(); }
|
|
|
|
|
-
|
|
|
|
|
- // Blocks (cooperatively - ulTaskNotifyTake sleeps this task, not the CPU)
|
|
|
|
|
- // until update() wakes it, runs one full refresh, then goes back to
|
|
|
|
|
- // sleep. Never returns; this is the task's entire body for its lifetime.
|
|
|
|
|
- [[noreturn]] void update_task_loop_() {
|
|
|
|
|
- // Arduino.h's delay()/yield() and SPI.h's transfer() call
|
|
|
|
|
- // esphome::App.feed_wdt() to keep this task's own long busy-waits and
|
|
|
|
|
- // SPI write bursts from starving another task's watchdog - but
|
|
|
|
|
- // feed_wdt() only resets the *calling* task's own subscription, and
|
|
|
|
|
- // this task was never subscribed (only the main loop task is, by
|
|
|
|
|
- // esphome's own setup). Every one of those calls was silently failing
|
|
|
|
|
- // (esp_task_wdt_reset() logging "task not found"), which is at best
|
|
|
|
|
- // useless and, going by a crash inside the same watchdog subsystem's
|
|
|
|
|
- // idle-task hook right after a burst of that error, plausibly worse
|
|
|
|
|
- // than useless. Subscribe for real so those resets land.
|
|
|
|
|
- esp_task_wdt_add(nullptr);
|
|
|
|
|
- while (true) {
|
|
|
|
|
- ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
|
|
|
|
- uint32_t start = millis();
|
|
|
|
|
- this->driver_->setFullWindow();
|
|
|
|
|
- this->driver_->firstPage();
|
|
|
|
|
- bool first_page = true;
|
|
|
|
|
- do {
|
|
|
|
|
- if (!first_page) {
|
|
|
|
|
- // update() already returned after the first page, so the caller
|
|
|
|
|
- // task is off running its own thing again - nothing keeps this
|
|
|
|
|
- // page's draw lambda from racing whatever it's doing (reading a
|
|
|
|
|
- // sensor, updating a global, ...). Freeze it just long enough to
|
|
|
|
|
- // draw this page too, so every page's lambda gets the same
|
|
|
|
|
- // "nothing else is running" guarantee the first one had for
|
|
|
|
|
- // free, not just the first. Resumed right after, before the
|
|
|
|
|
- // (slow) hardware write below, so the caller stays responsive
|
|
|
|
|
- // through the part that actually takes a while.
|
|
|
|
|
- vTaskSuspend(this->caller_task_);
|
|
|
|
|
- }
|
|
|
|
|
- this->do_update_();
|
|
|
|
|
- if (first_page) {
|
|
|
|
|
- // update() is waiting on exactly this: the draw lambda for the
|
|
|
|
|
- // first (often only) page has finished, so the caller can
|
|
|
|
|
- // proceed while the SPI write and physical refresh below run
|
|
|
|
|
- // without it.
|
|
|
|
|
- xSemaphoreGive(this->lambda_done_sem_);
|
|
|
|
|
- first_page = false;
|
|
|
|
|
- } else {
|
|
|
|
|
- vTaskResume(this->caller_task_);
|
|
|
|
|
- }
|
|
|
|
|
- } while (this->driver_->nextPage());
|
|
|
|
|
- ESP_LOGD(TAG, "Display update took %u ms", (unsigned) (millis() - start));
|
|
|
|
|
- this->update_running_.store(false, std::memory_order_release);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- TaskHandle_t update_task_{nullptr};
|
|
|
|
|
- // Set fresh by update() every call, before update_task_ can possibly read
|
|
|
|
|
- // it (it's blocked on lambda_done_sem_ until the first page finishes) -
|
|
|
|
|
- // safe without extra synchronization since update_running_ already
|
|
|
|
|
- // guarantees only one update cycle, and thus only one writer/reader pair
|
|
|
|
|
- // for this field, is ever in flight at a time.
|
|
|
|
|
- TaskHandle_t caller_task_{nullptr};
|
|
|
|
|
- SemaphoreHandle_t lambda_done_sem_{nullptr};
|
|
|
|
|
- std::atomic<bool> update_running_{false};
|
|
|
|
|
|
|
+ int get_width_internal() override { return PANEL_213_Z98C.width; }
|
|
|
|
|
+ int get_height_internal() override { return PANEL_213_Z98C.height; }
|
|
|
|
|
+
|
|
|
|
|
+ GPIOPin *cs_{nullptr};
|
|
|
|
|
+ GPIOPin *dc_{nullptr};
|
|
|
|
|
+ GPIOPin *rst_{nullptr};
|
|
|
|
|
+ GPIOPin *busy_{nullptr};
|
|
|
|
|
+ EpdSPIDevice spi_;
|
|
|
|
|
+ CycleEngine engine_;
|
|
|
|
|
+ uint8_t *plane0_{nullptr};
|
|
|
|
|
+ uint8_t *plane1_{nullptr};
|
|
|
|
|
+ uint32_t plane_bytes_{0};
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
} // namespace esphome::gxepd2
|
|
} // namespace esphome::gxepd2
|