Explorar el Código

Hook deep_sleep/safe_reboot via Component::on_powerdown(), add idle()

ESPHome calls on_powerdown() on every component right before deep_sleep
suspends the MCU (and on safe_reboot()) - confirmed in the vendored esphome
core (Application::run_powerdown_hooks(), deep_sleep's should_teardown_()
unconditional on every supported platform). Override it to call hibernate()
when a reset pin is configured, else power_off() - no YAML/lambda needed for
the common case, no engine change, since hibernate()'s "no paired wake call
needed" property already made this free.

Deliberately skips a Component::teardown() override to wait out a mid-flight
refresh: ESPHome's teardown budget is fixed by the caller and short (1s
reboot, 5s deep sleep) - sized for a network flush, not a 45-60s panel
refresh - so stalling teardown would always lose that race and get
force-advanced anyway. Exposed GxEPD2Display::idle() publicly instead, so a
caller who cares about the race (or a more complex application tracking its
own update sequence) can check it and decide for themselves.

Compiled clean against both esp32-arduino and esp32-idf test configs;
check_descriptors.py unaffected (61/61).
ZiB hace 2 semanas
padre
commit
7bd500fbca
Se han modificado 2 ficheros con 73 adiciones y 4 borrados
  1. 33 2
      CONTRIBUTING.md
  2. 40 2
      components/gxepd2/gxepd2_display.h

+ 33 - 2
CONTRIBUTING.md

@@ -183,14 +183,45 @@ pulse, no init/window/plane-write, just that one command table then `DONE`.
 Not run automatically by `request_full_update()`: this engine does a full
 hardware reset every cycle regardless of prior power state, so correctness
 never depends on calling either - they exist purely for panel longevity
-(reduced fading between infrequent updates) and, eventually, MCU-side
-deep-sleep pairing. `request_hibernate()` runs `power_off_sequence` then
+(reduced fading between infrequent updates) and MCU-side deep-sleep pairing
+(see below). `request_hibernate()` runs `power_off_sequence` then
 `hibernate_sequence` (hibernate()'s *tail* only, past whatever it shares with
 powerOff() - see below); no paired "wake" call exists or is needed, since
 `request_full_update()`'s `RESET_LOW` is an unconditional hardware reset pulse
 every cycle, which is exactly how GxEPD2 wakes a hibernating (RST-wakeable)
 panel.
 
+**Deep-sleep pairing.** `GxEPD2Display::on_powerdown()` overrides
+`Component::on_powerdown()`, which ESPHome calls on every component right
+before `deep_sleep` actually suspends the MCU, and on `safe_reboot()` (see
+`Application::run_powerdown_hooks()`; `deep_sleep`'s `should_teardown_()` is
+unconditional on every supported platform, so this always fires before a
+sleep). It calls `hibernate()` if a reset pin is configured, else
+`power_off()` - no YAML/lambda needed for the common case, and no engine
+change, since `hibernate()`'s "no paired wake call needed" property already
+makes this free.
+
+This is deliberately NOT paired with a `Component::teardown()` override to
+wait out a mid-flight refresh. ESPHome's teardown budget is short and fixed
+by the *caller*, not by us - 1s on `safe_reboot()`
+(`Application::TEARDOWN_TIMEOUT_REBOOT_MS`), 5s on `deep_sleep`
+(`TEARDOWN_TIMEOUT_DEEP_SLEEP_MS`) - and it's a hard cap: `teardown_components()`
+busy-polls every component's `teardown()` until all return `true` or the
+budget expires, then logs one warning per straggler and proceeds regardless.
+That budget is sized for a network flush (see `api_server::teardown()`,
+one of only two overrides in ESPHome core, both networking), not a hardware
+operation - it's far shorter than this engine's slower `busy_timeout_ms`
+classes (up to 45-60s for 4bpp/7-color panels), so stalling `teardown()`
+here would just lose that race every time and get force-advanced anyway.
+If a refresh is still running when `on_powerdown()` fires,
+`power_off()`/`hibernate()`'s own idle-check skips it with a warning instead.
+`GxEPD2Display::idle()` (wraps `CycleEngine::idle()`) is exposed publicly for
+exactly this: a caller who cares about the race - or a more complex
+application tracking its own multi-step update sequence - can check it and
+decide for themselves whether to hold off triggering deep sleep, rather than
+the component trying to enforce a resolution the framework itself won't
+support.
+
 Verifying `power_off()`/`hibernate()` actually took effect needs the panel on
 a bench, since the real effect is electrical (driving voltage off, or deep
 sleep) - nothing the MCU can see directly. `hibernate()`'s documented

+ 40 - 2
components/gxepd2/gxepd2_display.h

@@ -258,12 +258,21 @@ class GxEPD2Display : public display::DisplayBuffer {
 
   void loop() override { this->engine_.step(); }
 
+  // True once the engine has returned to idle after the last update()/
+  // power_off()/hibernate() call. Lets a more complex application (a lambda
+  // chaining several updates, a deep_sleep automation deciding whether to
+  // wait) check "is the panel still mid-refresh" instead of guessing from
+  // timing - the same thing power_off()/hibernate()'s own guards below check
+  // internally.
+  bool idle() const { return this->engine_.idle(); }
+
   // Turns off the panel's driving-voltage generation (reduces fading over
   // time between refreshes). Call from a lambda, e.g.
   // id(<display_id>).power_off(); - not called automatically by update(),
   // since this engine does a full hardware reset every cycle regardless of
-  // prior power state, so correctness never depends on it. A no-op (with a
-  // warning) while a refresh is still in progress.
+  // prior power state, so correctness never depends on it. (It IS called
+  // automatically from on_powerdown() below.) A no-op (with a warning) while
+  // a refresh is still in progress.
   void power_off() {
     if (!this->engine_.idle()) {
       ESP_LOGW(TAG, "Skipping power_off: display refresh still in progress");
@@ -284,6 +293,35 @@ class GxEPD2Display : public display::DisplayBuffer {
     this->engine_.request_hibernate();
   }
 
+  // ESPHome calls this on every component right before deep_sleep actually
+  // suspends the MCU, and on safe_reboot() - see
+  // Application::run_powerdown_hooks() (fires after teardown_components()
+  // completes or times out, deep_sleep's should_teardown_() is unconditional
+  // on every supported platform). Powers the panel down with no YAML/lambda
+  // needed for the common case: hibernate() when a reset pin is wired (the
+  // next update()'s unconditional RESET_LOW wakes it - see hibernate()
+  // above), else power_off() (needs no reset pulse to recover, so it's safe
+  // with no rst pin at all).
+  //
+  // Deliberately NOT paired with a teardown() override to wait out a
+  // mid-flight refresh: ESPHome's teardown budget (1s on reboot, 5s on deep
+  // sleep) is far shorter than this engine's slower busy_timeout_ms classes
+  // (up to 45-60s for 4bpp/7-color panels), so stalling teardown here would
+  // just lose that race every time and get force-advanced anyway - the
+  // budget is sized for a network flush (see api_server::teardown()), not a
+  // hardware operation. If a refresh is still running when this fires,
+  // power_off()/hibernate()'s own guard above skips it with a warning rather
+  // than fighting the panel for the bus mid-write. A caller who cares about
+  // that race can check idle() (above) and hold off triggering deep sleep
+  // themselves.
+  void on_powerdown() override {
+    if (this->rst_ != nullptr) {
+      this->hibernate();
+    } else {
+      this->power_off();
+    }
+  }
+
 #ifdef USE_GXEPD2_DIAGNOSTICS
   // Dev-time diagnostics - compiled out entirely unless this display's YAML
   // sets `diagnostics: true` (display.py then adds -DUSE_GXEPD2_DIAGNOSTICS),