Jelajahi Sumber

Capture power_off/hibernate for all panels; add dev-time diagnostics gate

- epd_command.h: power_off_sequence/hibernate_sequence fields on
  EpdPanelDescriptor (both optional, default nullptr/0 - a safe no-op).
- emit_descriptor.h: capture_power_sequence/capture_power_off_and_hibernate
  (each needs its own fresh driver session - every GxEPD2 driver's
  hibernate() calls powerOff() internally, so one session would silently
  drop the second call's commands) and print_power_sequences for splicing
  into an existing panel_*.h. tools/capture.py --power exposes it.
- Captured and added to all 15 panels. Three SSD16xx-family panels
  (154_Z90c, 213_Z98C, 426_GDEQ0426T82) have an empty power_off_sequence -
  real, not a bug: their _Update_Full() already sets _power_is_on = false,
  so powerOff()'s own guard has nothing left to send.
- cycle_engine.h: POWER_OFF_CMD/HIBERNATE_CMD states reusing the existing
  step_sequence_ command-table walker; request_power_off()/
  request_hibernate() are standalone entry points (no reset pulse - no
  paired "wake" call exists or is needed, since request_full_update()'s
  RESET_LOW is an unconditional reset every cycle anyway).
- gxepd2_display.h: power_off()/hibernate() exposed for a user's own
  lambda.
- New `diagnostics: true` display option (display.py) compiles in
  GxEPD2Display's dev-time-only diagnostic methods (currently
  diag_update_no_reset(), for bench-checking hibernate()'s "non-responsive
  until reset" contract using the screen as the oracle) behind
  -DUSE_GXEPD2_DIAGNOSTICS, so a production build pays nothing for them.
  Off by default.
- 5 new mono panels (213_B72/270/290_T5D/420/583) covering 5 previously
  unrepresented controller families, all structural/compile-verified.
- emit_descriptor.h bug fix: the plane-invert/1bpp probe drove its test
  fill through writeScreenBuffer, assuming it shares a command with the
  real plane-write (writeImage) - GxEPD2_270 doesn't (0x13 vs 0x15), so the
  probe silently misclassified a genuinely 1bpp panel as packed. Fixed to
  probe through writeImage instead.
- emit_descriptor.h: make_resolver_bw_packed, a fallback packed-palette
  resolver for a driver with no colour wrapper to drive (unlike the
  4-colour/7-colour resolvers) - covers GxEPD2_583/UC8159c's
  _send8pixel-style internal packing with no source reading needed.
- tools/capture.py --sweep/--diag: run the window-encoder sweep / Pattern-A
  diagnostic fill generically for any capturable model, no hand-written
  run_<panel>[_diag].cpp needed.

CONTRIBUTING.md and the harness README updated throughout.
ZiB 2 minggu lalu
induk
melakukan
d4f301c61b

+ 59 - 0
CONTRIBUTING.md

@@ -168,6 +168,46 @@ Every command and data byte is its own SPI transaction with CS toggled around
 it — wasteful versus a burst transfer, but deliberate: matching the vendor
 it — wasteful versus a burst transfer, but deliberate: matching the vendor
 `_writeCommand()`/`_writeData()` cycle byte-for-byte is the point.
 `_writeCommand()`/`_writeData()` cycle byte-for-byte is the point.
 
 
+`power_off_sequence`/`hibernate_sequence` are a separate, standalone entry
+point (`CycleEngine::request_power_off()`/`request_hibernate()`, exposed as
+`GxEPD2Display::power_off()`/`hibernate()` for a user's own lambda) - no reset
+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
+`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.
+
+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
+contract (non-responsive until the next RST pulse) is still bench-checkable
+with no probes, using the screen as the oracle: call `hibernate()`, then
+`GxEPD2Display::diag_update_no_reset()` (dev builds only - see below) with a
+pattern distinct from what's on screen; if hibernate took, that pattern
+shouldn't render (and BUSY may behave oddly). A normal `update()` right after
+should then wake the panel and render correctly, confirming both the "off"
+and the "wakes via reset" halves and leaving the panel in a known state.
+`power_off()` alone doesn't make the panel unresponsive (the next real
+refresh auto-powers back on), so this trick doesn't apply to it - its effect
+is only visible as reduced fading over long idle periods, or lower current
+draw if your bench supply shows it live.
+
+### Dev-time diagnostics (`diagnostics: true`)
+
+A stash for throwaway bench tests that would otherwise cost every production
+build flash space for no benefit. Set `diagnostics: true` on the `gxepd2`
+display in YAML (default `false`) to add `-DUSE_GXEPD2_DIAGNOSTICS`, which
+compiles in `GxEPD2Display`'s `#ifdef`-gated diagnostic methods - currently
+just `diag_update_no_reset()` (drives a full update, skipping the reset
+pulse - see above). Add more here as bench work needs them; this isn't a
+stable API and nothing here needs to stay once a test has done its job.
+
 ## Descriptor format (`epd_command.h`)
 ## Descriptor format (`epd_command.h`)
 
 
 `EpdPanelDescriptor` fields: `width`/`height`; `busy_active_level` +
 `EpdPanelDescriptor` fields: `width`/`height`; `busy_active_level` +
@@ -181,6 +221,25 @@ data, so a 4bpp 7-colour (ACeP) panel is just a palette plus `bits_per_pixel: 4`
 with no new engine flag (`GXEPD2_PALETTE_7C` is written, awaiting a captured
 with no new engine flag (`GXEPD2_PALETTE_7C` is written, awaiting a captured
 descriptor).
 descriptor).
 
 
+`power_off_sequence`/`hibernate_sequence` (both optional, default nullptr/0 -
+a safe no-op) are captured the same way as the other command tables - see
+`capture_power_sequence`/`capture_power_off_and_hibernate` in
+`emit_descriptor.h`. Every `GxEPD2_EPD` subclass implements `powerOff()`/
+`hibernate()` (both pure virtual in the base class), so this isn't optional
+per panel the way a colour family is; what *does* vary is whether `powerOff()`
+has anything left to do - several SSD16xx-family drivers (154_Z90c, 213_Z98C,
+426_GDEQ0426T82) fold power-down into their own full-refresh waveform
+(`_Update_Full()` sets `_power_is_on = false` itself), so `powerOff()`'s own
+guard finds nothing to send and `power_off_sequence` captures empty - correct,
+not a capture failure. `hibernate()` on every driver observed so far is
+`powerOff()` + exactly one more command (the deep-sleep trigger, gated on
+`rst >= 0`); `hibernate_sequence` stores only that tail, not a duplicate of
+`power_off_sequence`'s commands, which is why each needs its own **fresh**
+driver instance replayed through the normal init/clear/write/refresh setup
+before the call being captured - capturing both from one live session would
+have the second call's internal `powerOff()` silently emit nothing (already
+off from the first).
+
 ## Color and bit-depth model
 ## Color and bit-depth model
 
 
 Buffers map color literally. For **1bpp** panels `draw_absolute_pixel_internal`
 Buffers map color literally. For **1bpp** panels `draw_absolute_pixel_internal`

+ 91 - 15
components/gxepd2/cycle_engine.h

@@ -26,6 +26,11 @@ using EpdSPIDevice = spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARIT
 // top-to-bottom call stack. step() never blocks: every state does a small
 // top-to-bottom call stack. step() never blocks: every state does a small
 // bounded unit of work and returns, so nothing here can starve another task or
 // bounded unit of work and returns, so nothing here can starve another task or
 // need a watchdog subscription.
 // need a watchdog subscription.
+//
+// request_power_off()/request_hibernate() are a separate, standalone entry
+// point (no reset pulse, no init/window/plane-write) replaying just
+// power_off_sequence/hibernate_sequence - for a user's own lambda, not run
+// automatically by request_full_update(). See epd_command.h.
 class CycleEngine {
 class CycleEngine {
  public:
  public:
   void begin(const EpdPanelDescriptor *desc, GPIOPin *cs, GPIOPin *dc, GPIOPin *rst, GPIOPin *busy,
   void begin(const EpdPanelDescriptor *desc, GPIOPin *cs, GPIOPin *dc, GPIOPin *rst, GPIOPin *busy,
@@ -49,23 +54,47 @@ class CycleEngine {
   // Arms the state machine; returns immediately. planes[1] is ignored when
   // Arms the state machine; returns immediately. planes[1] is ignored when
   // desc->plane_count == 1. Undefined to call while !idle().
   // desc->plane_count == 1. Undefined to call while !idle().
   void request_full_update(const uint8_t *plane0, const uint8_t *plane1) {
   void request_full_update(const uint8_t *plane0, const uint8_t *plane1) {
-    this->planes_[0] = plane0;
-    this->planes_[1] = plane1;
-    uint32_t w = this->desc_->window_per_plane ? this->desc_->plane_width : this->desc_->width;
-    // width*height*bpp/8: /8 at 1bpp, /4 at 2bpp, /2 at 4bpp.
-    this->plane_bytes_ = (uint32_t) w * this->desc_->height * this->desc_->bits_per_pixel / 8;
-    this->bytes_per_row_ = this->plane_bytes_ / this->desc_->height;
-    // Wire length can exceed the buffer: PAIRED_EDGES sends each row twice
-    // (2x-height RAM). plane_bytes_ stays the real buffer size; wire_bytes_
-    // is what actually goes out on 0x10/0x24/... per plane.
-    uint32_t wire_lines = this->desc_->height;
-    if (this->desc_->line_order == LineOrder::PAIRED_EDGES)
-      wire_lines *= 2;
-    this->wire_bytes_ = wire_lines * this->bytes_per_row_;
+    this->arm_full_update_(plane0, plane1, this->rst_ != nullptr ? State::RESET_LOW : State::INIT_CMD);
+  }
+
+#ifdef USE_GXEPD2_DIAGNOSTICS
+  // Dev-time diagnostic, compiled out unless the display's `diagnostics:
+  // true` is set (display.py adds -DUSE_GXEPD2_DIAGNOSTICS): arms a full
+  // update starting at INIT_CMD, skipping the reset pulse entirely - the
+  // same path a panel with no reset pin wired already takes every cycle.
+  // Lets a bench test use the screen as the oracle for "is the panel
+  // actually non-responsive right now" (e.g. right after hibernate()): if a
+  // distinct pattern drawn this way does NOT appear, the prior power call
+  // really did take the panel off-line; a normal request_full_update() right
+  // after (reset pulse included) should then wake it and render correctly.
+  void diag_request_update_no_reset(const uint8_t *plane0, const uint8_t *plane1) {
+    this->arm_full_update_(plane0, plane1, State::INIT_CMD);
+  }
+#endif
+
+  // Arms a standalone powerOff() replay - no reset pulse, no init/window/
+  // plane write, just the captured power_off_sequence (if any) then DONE.
+  // Undefined to call while !idle(). Safe to call repeatedly: GxEPD2's own
+  // powerOff() is itself guarded on "already off," which the capture already
+  // baked in (see make_power_sequence_resolvers in emit_descriptor.h).
+  void request_power_off() {
+    this->cursor_ = 0;
+    this->cmd_phase_ = CmdPhase::BEFORE_DELAY;
+    this->hibernating_ = false;
+    this->set_state_(State::POWER_OFF_CMD);
+  }
+
+  // Arms a standalone hibernate() replay: power_off_sequence then
+  // hibernate_sequence (the deep-sleep trigger only - see epd_command.h).
+  // Undefined to call while !idle(). No paired "wake" call exists or is
+  // needed: 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.
+  void request_hibernate() {
     this->cursor_ = 0;
     this->cursor_ = 0;
-    this->plane_ = 0;
     this->cmd_phase_ = CmdPhase::BEFORE_DELAY;
     this->cmd_phase_ = CmdPhase::BEFORE_DELAY;
-    this->set_state_(this->rst_ != nullptr ? State::RESET_LOW : State::INIT_CMD);
+    this->hibernating_ = true;
+    this->set_state_(State::POWER_OFF_CMD);
   }
   }
 
 
   bool idle() const { return this->state_ == State::IDLE; }
   bool idle() const { return this->state_ == State::IDLE; }
@@ -135,6 +164,19 @@ class CycleEngine {
                               State::DONE);
                               State::DONE);
         return;
         return;
 
 
+      case State::POWER_OFF_CMD:
+        // request_hibernate() routes through here first (hibernate() is
+        // itself powerOff() + one more command on every GxEPD2 driver - see
+        // epd_command.h) - done_state branches to HIBERNATE_CMD only then.
+        this->step_sequence_(this->desc_->power_off_sequence, this->desc_->power_off_sequence_len,
+                              State::POWER_OFF_CMD, this->hibernating_ ? State::HIBERNATE_CMD : State::DONE);
+        return;
+
+      case State::HIBERNATE_CMD:
+        this->step_sequence_(this->desc_->hibernate_sequence, this->desc_->hibernate_sequence_len,
+                              State::HIBERNATE_CMD, State::DONE);
+        return;
+
       case State::POST_DELAY:
       case State::POST_DELAY:
         if (millis() < this->deadline_)
         if (millis() < this->deadline_)
           return;
           return;
@@ -167,6 +209,11 @@ class CycleEngine {
   }
   }
 
 
  private:
  private:
+  // State/CmdPhase must come before arm_full_update_() below: a member
+  // function's PARAMETER types (unlike its body) are resolved at the point
+  // of declaration, not deferred to complete-class context, so `State` has
+  // to already be a known type by the time arm_full_update_()'s signature is
+  // parsed.
   enum class State {
   enum class State {
     IDLE,
     IDLE,
     RESET_LOW,
     RESET_LOW,
@@ -177,6 +224,8 @@ class CycleEngine {
     SET_WINDOW,
     SET_WINDOW,
     WRITE_PLANE,
     WRITE_PLANE,
     REFRESH_CMD,
     REFRESH_CMD,
+    POWER_OFF_CMD,
+    HIBERNATE_CMD,
     POST_DELAY,
     POST_DELAY,
     WAIT_BUSY,
     WAIT_BUSY,
     DONE,
     DONE,
@@ -184,6 +233,28 @@ class CycleEngine {
   };
   };
   enum class CmdPhase { BEFORE_DELAY, ISSUE };
   enum class CmdPhase { BEFORE_DELAY, ISSUE };
 
 
+  // Shared by request_full_update() and (dev builds only) the no-reset
+  // diagnostic path above - identical setup, different starting state.
+  void arm_full_update_(const uint8_t *plane0, const uint8_t *plane1, State start_state) {
+    this->planes_[0] = plane0;
+    this->planes_[1] = plane1;
+    uint32_t w = this->desc_->window_per_plane ? this->desc_->plane_width : this->desc_->width;
+    // width*height*bpp/8: /8 at 1bpp, /4 at 2bpp, /2 at 4bpp.
+    this->plane_bytes_ = (uint32_t) w * this->desc_->height * this->desc_->bits_per_pixel / 8;
+    this->bytes_per_row_ = this->plane_bytes_ / this->desc_->height;
+    // Wire length can exceed the buffer: PAIRED_EDGES sends each row twice
+    // (2x-height RAM). plane_bytes_ stays the real buffer size; wire_bytes_
+    // is what actually goes out on 0x10/0x24/... per plane.
+    uint32_t wire_lines = this->desc_->height;
+    if (this->desc_->line_order == LineOrder::PAIRED_EDGES)
+      wire_lines *= 2;
+    this->wire_bytes_ = wire_lines * this->bytes_per_row_;
+    this->cursor_ = 0;
+    this->plane_ = 0;
+    this->cmd_phase_ = CmdPhase::BEFORE_DELAY;
+    this->set_state_(start_state);
+  }
+
   // Every command byte and every data byte is its own SPI transaction, CS
   // Every command byte and every data byte is its own SPI transaction, CS
   // toggled around each one - this looks wasteful, but it's mechanically
   // toggled around each one - this looks wasteful, but it's mechanically
   // what GxEPD2_EPD::_writeCommand()/_writeData(uint8_t) do (see
   // what GxEPD2_EPD::_writeCommand()/_writeData(uint8_t) do (see
@@ -404,6 +475,10 @@ class CycleEngine {
         return "WRITE_PLANE";
         return "WRITE_PLANE";
       case State::REFRESH_CMD:
       case State::REFRESH_CMD:
         return "REFRESH_CMD";
         return "REFRESH_CMD";
+      case State::POWER_OFF_CMD:
+        return "POWER_OFF_CMD";
+      case State::HIBERNATE_CMD:
+        return "HIBERNATE_CMD";
       case State::POST_DELAY:
       case State::POST_DELAY:
         return "POST_DELAY";
         return "POST_DELAY";
       case State::WAIT_BUSY:
       case State::WAIT_BUSY:
@@ -435,6 +510,7 @@ class CycleEngine {
   State delay_return_state_{State::DONE};
   State delay_return_state_{State::DONE};
   uint32_t busy_wait_deadline_{0};
   uint32_t busy_wait_deadline_{0};
   State busy_wait_next_{State::DONE};
   State busy_wait_next_{State::DONE};
+  bool hibernating_{false};  // POWER_OFF_CMD's done_state: HIBERNATE_CMD if set, else DONE
 };
 };
 
 
 }  // namespace esphome::gxepd2
 }  // namespace esphome::gxepd2

+ 13 - 0
components/gxepd2/display.py

@@ -41,6 +41,12 @@ DEPENDENCIES = ["spi"]
 # No effect on 2bpp/4bpp packed panels - there the palette maps literal RGB.
 # No effect on 2bpp/4bpp packed panels - there the palette maps literal RGB.
 CONF_INVERT_MONO = "invert_mono"
 CONF_INVERT_MONO = "invert_mono"
 
 
+# Compiles in GxEPD2Display's dev-time diagnostic methods (currently
+# diag_update_no_reset() - see gxepd2_display.h), stashed behind this flag so
+# a production build doesn't pay flash for bench-only tooling. Off by
+# default; set true only on the device you're actively debugging.
+CONF_DIAGNOSTICS = "diagnostics"
+
 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)
 
 
@@ -102,6 +108,7 @@ CONFIG_SCHEMA = cv.All(
             # to_code() (just False today; true for a waveshare alias once that
             # to_code() (just False today; true for a waveshare alias once that
             # lands), while an explicit value always wins.
             # lands), while an explicit value always wins.
             cv.Optional(CONF_INVERT_MONO): cv.boolean,
             cv.Optional(CONF_INVERT_MONO): cv.boolean,
+            cv.Optional(CONF_DIAGNOSTICS, default=False): cv.boolean,
         }
         }
     ),
     ),
     # The runtime C++ uses only framework-portable ESPHome abstractions, so
     # The runtime C++ uses only framework-portable ESPHome abstractions, so
@@ -137,6 +144,12 @@ async def to_code(config: ConfigType) -> None:
     cg.add(var.set_model_name(_panel_label(panel.part)))
     cg.add(var.set_model_name(_panel_label(panel.part)))
     cg.add(var.set_verified(panel.status == "verified"))
     cg.add(var.set_verified(panel.status == "verified"))
 
 
+    # Compiles in GxEPD2Display/CycleEngine's dev-time diagnostic methods -
+    # see CONF_DIAGNOSTICS above. Not set at all (not just defined false) for
+    # every other build, so a production build's flash never sees them.
+    if config[CONF_DIAGNOSTICS]:
+        cg.add_define("USE_GXEPD2_DIAGNOSTICS")
+
     # A top-level `lambda:` must be processed and attached via set_writer()
     # A top-level `lambda:` must be processed and attached via set_writer()
     # here - register_display() only wires up `pages:` (same pattern as every
     # here - register_display() only wires up `pages:` (same pattern as every
     # other display component's to_code(), e.g. epaper_spi).
     # other display component's to_code(), e.g. epaper_spi).

+ 16 - 0
components/gxepd2/epd_command.h

@@ -157,6 +157,22 @@ struct EpdPanelDescriptor {
 
 
   const EpdCommand *refresh_sequence;  // e.g. 0x22/0xF7 then trigger 0x20
   const EpdCommand *refresh_sequence;  // e.g. 0x22/0xF7 then trigger 0x20
   uint8_t refresh_sequence_len;
   uint8_t refresh_sequence_len;
+
+  // powerOff()/hibernate(), captured the same way as refresh_sequence - see
+  // make_power_sequence_resolvers in emit_descriptor.h. Exposed for a user's
+  // own lambda (CycleEngine::request_power_off/request_hibernate), not run
+  // automatically by update(): this engine does a full hardware reset on
+  // every cycle regardless of prior power state, so correctness never
+  // depends on calling either. hibernate_sequence is the deep-sleep trigger
+  // ONLY (every GxEPD2 driver's hibernate() is powerOff() + one more
+  // command when rst is wired) - CycleEngine runs power_off_sequence first,
+  // then hibernate_sequence, to replay the same two-step effect. Both
+  // nullptr/0 (not yet captured) is a safe no-op: CycleEngine treats a
+  // zero-length sequence as already done.
+  const EpdCommand *power_off_sequence{nullptr};
+  uint8_t power_off_sequence_len{0};
+  const EpdCommand *hibernate_sequence{nullptr};  // deep-sleep trigger only, after power_off_sequence
+  uint8_t hibernate_sequence_len{0};
 };
 };
 
 
 }  // namespace esphome::gxepd2
 }  // namespace esphome::gxepd2

+ 51 - 0
components/gxepd2/gxepd2_display.h

@@ -213,6 +213,57 @@ class GxEPD2Display : public display::DisplayBuffer {
 
 
   void loop() override { this->engine_.step(); }
   void loop() override { this->engine_.step(); }
 
 
+  // 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.
+  void power_off() {
+    if (!this->engine_.idle()) {
+      ESP_LOGW(TAG, "Skipping power_off: display refresh still in progress");
+      return;
+    }
+    this->engine_.request_power_off();
+  }
+
+  // power_off() + deep sleep, wakeable only by a hardware reset pulse - which
+  // the next update() always issues (request_full_update()'s RESET_LOW), so
+  // no paired "wake" call is needed; just call update() as usual. A no-op
+  // (with a warning) while a refresh is still in progress.
+  void hibernate() {
+    if (!this->engine_.idle()) {
+      ESP_LOGW(TAG, "Skipping hibernate: display refresh still in progress");
+      return;
+    }
+    this->engine_.request_hibernate();
+  }
+
+#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),
+  // so a production build pays nothing for them. A stash for throwaway bench
+  // tests, not a stable API - add more here as needed, don't feel obliged to
+  // keep any of it around once a test has done its job.
+  //
+  // Draws the current lambda/pages content and pushes it WITHOUT the reset
+  // pulse update() normally does first - see
+  // CycleEngine::diag_request_update_no_reset(). Call from a lambda, e.g. to
+  // check whether power_off()/hibernate() actually took: call one of those,
+  // then this with a pattern distinct from what's already on screen - no
+  // change (or odd BUSY behavior) means the panel really is off-line; follow
+  // with a normal update() to confirm the next real reset wakes it and to
+  // leave the panel in a known state either way.
+  void diag_update_no_reset() {
+    if (!this->engine_.idle()) {
+      ESP_LOGW(TAG, "Skipping diag_update_no_reset: display refresh still in progress");
+      return;
+    }
+    this->do_update_();
+    this->engine_.diag_request_update_no_reset(this->plane0_, this->plane1_);
+  }
+#endif
+
   void dump_config() override {
   void dump_config() override {
     ESP_LOGCONFIG(TAG, "GxEPD2 Display:");
     ESP_LOGCONFIG(TAG, "GxEPD2 Display:");
     ESP_LOGCONFIG(TAG, "  Dimensions: %ux%u", (unsigned) this->desc_->width, (unsigned) this->desc_->height);
     ESP_LOGCONFIG(TAG, "  Dimensions: %ux%u", (unsigned) this->desc_->width, (unsigned) this->desc_->height);

+ 8 - 0
components/gxepd2/panel_154_z90c.h

@@ -64,6 +64,12 @@ inline const EpdCommand PANEL_154_Z90C_WINDOW[] = {
     {0x4F, PANEL_154_Z90C_WINDOW_YC, 2},
     {0x4F, PANEL_154_Z90C_WINDOW_YC, 2},
 };
 };
 
 
+inline const uint8_t PANEL_154_Z90C_HIBERNATE_0_10[] = {0x01};
+
+inline const EpdCommand PANEL_154_Z90C_HIBERNATE[] = {
+    {0x10, PANEL_154_Z90C_HIBERNATE_0_10, 1},
+};
+
 inline const EpdPanelDescriptor PANEL_154_Z90C = {
 inline const EpdPanelDescriptor PANEL_154_Z90C = {
     .width = 200,
     .width = 200,
     .height = 200,
     .height = 200,
@@ -78,6 +84,8 @@ inline const EpdPanelDescriptor PANEL_154_Z90C = {
     .plane_invert = {false, false},  // see header note (matches 213 Z98c's validated convention)
     .plane_invert = {false, false},  // see header note (matches 213 Z98c's validated convention)
     .refresh_sequence = PANEL_154_Z90C_REFRESH,
     .refresh_sequence = PANEL_154_Z90C_REFRESH,
     .refresh_sequence_len = sizeof(PANEL_154_Z90C_REFRESH) / sizeof(PANEL_154_Z90C_REFRESH[0]),
     .refresh_sequence_len = sizeof(PANEL_154_Z90C_REFRESH) / sizeof(PANEL_154_Z90C_REFRESH[0]),
+    .hibernate_sequence = PANEL_154_Z90C_HIBERNATE,
+    .hibernate_sequence_len = sizeof(PANEL_154_Z90C_HIBERNATE) / sizeof(PANEL_154_Z90C_HIBERNATE[0]),
 };
 };
 
 
 }  // namespace esphome::gxepd2
 }  // namespace esphome::gxepd2

+ 17 - 0
components/gxepd2/panel_213_b72.h

@@ -72,6 +72,19 @@ inline const EpdCommand PANEL_213_B72_REFRESH[] = {
     {0x20, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},
     {0x20, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},
 };
 };
 
 
+inline const uint8_t PANEL_213_B72_POWER_OFF_0_22[] = {0xC3};
+
+inline const EpdCommand PANEL_213_B72_POWER_OFF[] = {
+    {0x22, PANEL_213_B72_POWER_OFF_0_22, 1},
+    {0x20, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},
+};
+
+inline const uint8_t PANEL_213_B72_HIBERNATE_0_10[] = {0x01};
+
+inline const EpdCommand PANEL_213_B72_HIBERNATE[] = {
+    {0x10, PANEL_213_B72_HIBERNATE_0_10, 1},
+};
+
 inline const EpdPanelDescriptor PANEL_213_B72 = {
 inline const EpdPanelDescriptor PANEL_213_B72 = {
     .width = 128,
     .width = 128,
     .height = 250,
     .height = 250,
@@ -89,6 +102,10 @@ inline const EpdPanelDescriptor PANEL_213_B72 = {
     .palette_len = 0,
     .palette_len = 0,
     .refresh_sequence = PANEL_213_B72_REFRESH,
     .refresh_sequence = PANEL_213_B72_REFRESH,
     .refresh_sequence_len = sizeof(PANEL_213_B72_REFRESH) / sizeof(PANEL_213_B72_REFRESH[0]),
     .refresh_sequence_len = sizeof(PANEL_213_B72_REFRESH) / sizeof(PANEL_213_B72_REFRESH[0]),
+    .power_off_sequence = PANEL_213_B72_POWER_OFF,
+    .power_off_sequence_len = sizeof(PANEL_213_B72_POWER_OFF) / sizeof(PANEL_213_B72_POWER_OFF[0]),
+    .hibernate_sequence = PANEL_213_B72_HIBERNATE,
+    .hibernate_sequence_len = sizeof(PANEL_213_B72_HIBERNATE) / sizeof(PANEL_213_B72_HIBERNATE[0]),
 };
 };
 
 
 }  // namespace esphome::gxepd2
 }  // namespace esphome::gxepd2

+ 8 - 0
components/gxepd2/panel_213_z98c.h

@@ -49,6 +49,12 @@ inline const EpdCommand PANEL_213_Z98C_WINDOW[] = {
     {0x4F, PANEL_213_Z98C_WINDOW_YC, 2},
     {0x4F, PANEL_213_Z98C_WINDOW_YC, 2},
 };
 };
 
 
+inline const uint8_t PANEL_213_Z98C_HIBERNATE_0_10[] = {0x01};
+
+inline const EpdCommand PANEL_213_Z98C_HIBERNATE[] = {
+    {0x10, PANEL_213_Z98C_HIBERNATE_0_10, 1},
+};
+
 // Vendor full_refresh_time is 15000ms; used as the ceiling for every busy
 // Vendor full_refresh_time is 15000ms; used as the ceiling for every busy
 // wait (erring long only delays reporting a genuine timeout).
 // wait (erring long only delays reporting a genuine timeout).
 inline const EpdPanelDescriptor PANEL_213_Z98C = {
 inline const EpdPanelDescriptor PANEL_213_Z98C = {
@@ -69,6 +75,8 @@ inline const EpdPanelDescriptor PANEL_213_Z98C = {
     .plane_invert = {false, false},
     .plane_invert = {false, false},
     .refresh_sequence = PANEL_213_Z98C_REFRESH,
     .refresh_sequence = PANEL_213_Z98C_REFRESH,
     .refresh_sequence_len = sizeof(PANEL_213_Z98C_REFRESH) / sizeof(PANEL_213_Z98C_REFRESH[0]),
     .refresh_sequence_len = sizeof(PANEL_213_Z98C_REFRESH) / sizeof(PANEL_213_Z98C_REFRESH[0]),
+    .hibernate_sequence = PANEL_213_Z98C_HIBERNATE,
+    .hibernate_sequence_len = sizeof(PANEL_213_Z98C_HIBERNATE) / sizeof(PANEL_213_Z98C_HIBERNATE[0]),
 };
 };
 
 
 }  // namespace esphome::gxepd2
 }  // namespace esphome::gxepd2

+ 16 - 0
components/gxepd2/panel_213c_gdey0213f51.h

@@ -82,6 +82,18 @@ inline const EpdCommand PANEL_213C_GDEY0213F51_REFRESH[] = {
      /*wait_busy_after=*/true},
      /*wait_busy_after=*/true},
 };
 };
 
 
+inline const uint8_t PANEL_213C_GDEY0213F51_POWER_OFF_0_02[] = {0x00};
+
+inline const EpdCommand PANEL_213C_GDEY0213F51_POWER_OFF[] = {
+    {0x02, PANEL_213C_GDEY0213F51_POWER_OFF_0_02, 1, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},
+};
+
+inline const uint8_t PANEL_213C_GDEY0213F51_HIBERNATE_0_07[] = {0xA5};
+
+inline const EpdCommand PANEL_213C_GDEY0213F51_HIBERNATE[] = {
+    {0x07, PANEL_213C_GDEY0213F51_HIBERNATE_0_07, 1},
+};
+
 inline const EpdPanelDescriptor PANEL_213C_GDEY0213F51 = {
 inline const EpdPanelDescriptor PANEL_213C_GDEY0213F51 = {
     .width = 128,
     .width = 128,
     .height = 250,
     .height = 250,
@@ -100,6 +112,10 @@ inline const EpdPanelDescriptor PANEL_213C_GDEY0213F51 = {
     .blank_code = 0x01,
     .blank_code = 0x01,
     .refresh_sequence = PANEL_213C_GDEY0213F51_REFRESH,
     .refresh_sequence = PANEL_213C_GDEY0213F51_REFRESH,
     .refresh_sequence_len = sizeof(PANEL_213C_GDEY0213F51_REFRESH) / sizeof(PANEL_213C_GDEY0213F51_REFRESH[0]),
     .refresh_sequence_len = sizeof(PANEL_213C_GDEY0213F51_REFRESH) / sizeof(PANEL_213C_GDEY0213F51_REFRESH[0]),
+    .power_off_sequence = PANEL_213C_GDEY0213F51_POWER_OFF,
+    .power_off_sequence_len = sizeof(PANEL_213C_GDEY0213F51_POWER_OFF) / sizeof(PANEL_213C_GDEY0213F51_POWER_OFF[0]),
+    .hibernate_sequence = PANEL_213C_GDEY0213F51_HIBERNATE,
+    .hibernate_sequence_len = sizeof(PANEL_213C_GDEY0213F51_HIBERNATE) / sizeof(PANEL_213C_GDEY0213F51_HIBERNATE[0]),
 };
 };
 
 
 }  // namespace esphome::gxepd2
 }  // namespace esphome::gxepd2

+ 16 - 0
components/gxepd2/panel_213c_gdey0213f52.h

@@ -45,6 +45,18 @@ inline const EpdCommand PANEL_213C_GDEY0213F52_REFRESH[] = {
      /*wait_busy_after=*/true},
      /*wait_busy_after=*/true},
 };
 };
 
 
+inline const uint8_t PANEL_213C_GDEY0213F52_POWER_OFF_0_02[] = {0x00};
+
+inline const EpdCommand PANEL_213C_GDEY0213F52_POWER_OFF[] = {
+    {0x02, PANEL_213C_GDEY0213F52_POWER_OFF_0_02, 1, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},
+};
+
+inline const uint8_t PANEL_213C_GDEY0213F52_HIBERNATE_0_07[] = {0xA5};
+
+inline const EpdCommand PANEL_213C_GDEY0213F52_HIBERNATE[] = {
+    {0x07, PANEL_213C_GDEY0213F52_HIBERNATE_0_07, 1},
+};
+
 inline const EpdPanelDescriptor PANEL_213C_GDEY0213F52 = {
 inline const EpdPanelDescriptor PANEL_213C_GDEY0213F52 = {
     .width = 128,
     .width = 128,
     .height = 250,
     .height = 250,
@@ -63,6 +75,10 @@ inline const EpdPanelDescriptor PANEL_213C_GDEY0213F52 = {
     .blank_code = 0x01,  // white; matches the observed 0x55 clear (code 01 x4)
     .blank_code = 0x01,  // white; matches the observed 0x55 clear (code 01 x4)
     .refresh_sequence = PANEL_213C_GDEY0213F52_REFRESH,
     .refresh_sequence = PANEL_213C_GDEY0213F52_REFRESH,
     .refresh_sequence_len = sizeof(PANEL_213C_GDEY0213F52_REFRESH) / sizeof(PANEL_213C_GDEY0213F52_REFRESH[0]),
     .refresh_sequence_len = sizeof(PANEL_213C_GDEY0213F52_REFRESH) / sizeof(PANEL_213C_GDEY0213F52_REFRESH[0]),
+    .power_off_sequence = PANEL_213C_GDEY0213F52_POWER_OFF,
+    .power_off_sequence_len = sizeof(PANEL_213C_GDEY0213F52_POWER_OFF) / sizeof(PANEL_213C_GDEY0213F52_POWER_OFF[0]),
+    .hibernate_sequence = PANEL_213C_GDEY0213F52_HIBERNATE,
+    .hibernate_sequence_len = sizeof(PANEL_213C_GDEY0213F52_HIBERNATE) / sizeof(PANEL_213C_GDEY0213F52_HIBERNATE[0]),
 };
 };
 
 
 }  // namespace esphome::gxepd2
 }  // namespace esphome::gxepd2

+ 14 - 0
components/gxepd2/panel_270.h

@@ -71,6 +71,16 @@ inline const EpdCommand PANEL_270_REFRESH[] = {
     {0x12, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},
     {0x12, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},
 };
 };
 
 
+inline const EpdCommand PANEL_270_POWER_OFF[] = {
+    {0x02, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},
+};
+
+inline const uint8_t PANEL_270_HIBERNATE_0_07[] = {0xA5};
+
+inline const EpdCommand PANEL_270_HIBERNATE[] = {
+    {0x07, PANEL_270_HIBERNATE_0_07, 1},
+};
+
 inline const EpdPanelDescriptor PANEL_270 = {
 inline const EpdPanelDescriptor PANEL_270 = {
     .width = 176,
     .width = 176,
     .height = 264,
     .height = 264,
@@ -88,6 +98,10 @@ inline const EpdPanelDescriptor PANEL_270 = {
     .palette_len = 0,
     .palette_len = 0,
     .refresh_sequence = PANEL_270_REFRESH,
     .refresh_sequence = PANEL_270_REFRESH,
     .refresh_sequence_len = sizeof(PANEL_270_REFRESH) / sizeof(PANEL_270_REFRESH[0]),
     .refresh_sequence_len = sizeof(PANEL_270_REFRESH) / sizeof(PANEL_270_REFRESH[0]),
+    .power_off_sequence = PANEL_270_POWER_OFF,
+    .power_off_sequence_len = sizeof(PANEL_270_POWER_OFF) / sizeof(PANEL_270_POWER_OFF[0]),
+    .hibernate_sequence = PANEL_270_HIBERNATE,
+    .hibernate_sequence_len = sizeof(PANEL_270_HIBERNATE) / sizeof(PANEL_270_HIBERNATE[0]),
 };
 };
 
 
 }  // namespace esphome::gxepd2
 }  // namespace esphome::gxepd2

+ 14 - 0
components/gxepd2/panel_290_t5d.h

@@ -55,6 +55,16 @@ inline const EpdCommand PANEL_290_T5D_REFRESH[] = {
     {0x12, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},
     {0x12, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},
 };
 };
 
 
+inline const EpdCommand PANEL_290_T5D_POWER_OFF[] = {
+    {0x02, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},
+};
+
+inline const uint8_t PANEL_290_T5D_HIBERNATE_0_07[] = {0xA5};
+
+inline const EpdCommand PANEL_290_T5D_HIBERNATE[] = {
+    {0x07, PANEL_290_T5D_HIBERNATE_0_07, 1},
+};
+
 inline const EpdPanelDescriptor PANEL_290_T5D = {
 inline const EpdPanelDescriptor PANEL_290_T5D = {
     .width = 128,
     .width = 128,
     .height = 296,
     .height = 296,
@@ -72,6 +82,10 @@ inline const EpdPanelDescriptor PANEL_290_T5D = {
     .palette_len = 0,
     .palette_len = 0,
     .refresh_sequence = PANEL_290_T5D_REFRESH,
     .refresh_sequence = PANEL_290_T5D_REFRESH,
     .refresh_sequence_len = sizeof(PANEL_290_T5D_REFRESH) / sizeof(PANEL_290_T5D_REFRESH[0]),
     .refresh_sequence_len = sizeof(PANEL_290_T5D_REFRESH) / sizeof(PANEL_290_T5D_REFRESH[0]),
+    .power_off_sequence = PANEL_290_T5D_POWER_OFF,
+    .power_off_sequence_len = sizeof(PANEL_290_T5D_POWER_OFF) / sizeof(PANEL_290_T5D_POWER_OFF[0]),
+    .hibernate_sequence = PANEL_290_T5D_HIBERNATE,
+    .hibernate_sequence_len = sizeof(PANEL_290_T5D_HIBERNATE) / sizeof(PANEL_290_T5D_HIBERNATE[0]),
 };
 };
 
 
 }  // namespace esphome::gxepd2
 }  // namespace esphome::gxepd2

+ 16 - 0
components/gxepd2/panel_290c_gdey029f51.h

@@ -79,6 +79,18 @@ inline const EpdCommand PANEL_290C_GDEY029F51_REFRESH[] = {
      /*wait_busy_after=*/true},
      /*wait_busy_after=*/true},
 };
 };
 
 
+inline const uint8_t PANEL_290C_GDEY029F51_POWER_OFF_0_02[] = {0x00};
+
+inline const EpdCommand PANEL_290C_GDEY029F51_POWER_OFF[] = {
+    {0x02, PANEL_290C_GDEY029F51_POWER_OFF_0_02, 1, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},
+};
+
+inline const uint8_t PANEL_290C_GDEY029F51_HIBERNATE_0_07[] = {0xA5};
+
+inline const EpdCommand PANEL_290C_GDEY029F51_HIBERNATE[] = {
+    {0x07, PANEL_290C_GDEY029F51_HIBERNATE_0_07, 1},
+};
+
 inline const EpdPanelDescriptor PANEL_290C_GDEY029F51 = {
 inline const EpdPanelDescriptor PANEL_290C_GDEY029F51 = {
     .width = 128,
     .width = 128,
     .height = 296,
     .height = 296,
@@ -97,6 +109,10 @@ inline const EpdPanelDescriptor PANEL_290C_GDEY029F51 = {
     .blank_code = 0x01,
     .blank_code = 0x01,
     .refresh_sequence = PANEL_290C_GDEY029F51_REFRESH,
     .refresh_sequence = PANEL_290C_GDEY029F51_REFRESH,
     .refresh_sequence_len = sizeof(PANEL_290C_GDEY029F51_REFRESH) / sizeof(PANEL_290C_GDEY029F51_REFRESH[0]),
     .refresh_sequence_len = sizeof(PANEL_290C_GDEY029F51_REFRESH) / sizeof(PANEL_290C_GDEY029F51_REFRESH[0]),
+    .power_off_sequence = PANEL_290C_GDEY029F51_POWER_OFF,
+    .power_off_sequence_len = sizeof(PANEL_290C_GDEY029F51_POWER_OFF) / sizeof(PANEL_290C_GDEY029F51_POWER_OFF[0]),
+    .hibernate_sequence = PANEL_290C_GDEY029F51_HIBERNATE,
+    .hibernate_sequence_len = sizeof(PANEL_290C_GDEY029F51_HIBERNATE) / sizeof(PANEL_290C_GDEY029F51_HIBERNATE[0]),
 };
 };
 
 
 }  // namespace esphome::gxepd2
 }  // namespace esphome::gxepd2

+ 14 - 0
components/gxepd2/panel_420.h

@@ -75,6 +75,16 @@ inline const EpdCommand PANEL_420_REFRESH[] = {
     {0x12, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},
     {0x12, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},
 };
 };
 
 
+inline const EpdCommand PANEL_420_POWER_OFF[] = {
+    {0x02, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},
+};
+
+inline const uint8_t PANEL_420_HIBERNATE_0_07[] = {0xA5};
+
+inline const EpdCommand PANEL_420_HIBERNATE[] = {
+    {0x07, PANEL_420_HIBERNATE_0_07, 1},
+};
+
 inline const EpdPanelDescriptor PANEL_420 = {
 inline const EpdPanelDescriptor PANEL_420 = {
     .width = 400,
     .width = 400,
     .height = 300,
     .height = 300,
@@ -92,6 +102,10 @@ inline const EpdPanelDescriptor PANEL_420 = {
     .palette_len = 0,
     .palette_len = 0,
     .refresh_sequence = PANEL_420_REFRESH,
     .refresh_sequence = PANEL_420_REFRESH,
     .refresh_sequence_len = sizeof(PANEL_420_REFRESH) / sizeof(PANEL_420_REFRESH[0]),
     .refresh_sequence_len = sizeof(PANEL_420_REFRESH) / sizeof(PANEL_420_REFRESH[0]),
+    .power_off_sequence = PANEL_420_POWER_OFF,
+    .power_off_sequence_len = sizeof(PANEL_420_POWER_OFF) / sizeof(PANEL_420_POWER_OFF[0]),
+    .hibernate_sequence = PANEL_420_HIBERNATE,
+    .hibernate_sequence_len = sizeof(PANEL_420_HIBERNATE) / sizeof(PANEL_420_HIBERNATE[0]),
 };
 };
 
 
 }  // namespace esphome::gxepd2
 }  // namespace esphome::gxepd2

+ 8 - 0
components/gxepd2/panel_426_gdeq0426t82.h

@@ -64,6 +64,12 @@ inline const EpdCommand PANEL_426_GDEQ0426T82_REFRESH[] = {
     {0x20, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},
     {0x20, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},
 };
 };
 
 
+inline const uint8_t PANEL_426_GDEQ0426T82_HIBERNATE_0_10[] = {0x01};
+
+inline const EpdCommand PANEL_426_GDEQ0426T82_HIBERNATE[] = {
+    {0x10, PANEL_426_GDEQ0426T82_HIBERNATE_0_10, 1},
+};
+
 inline const EpdPanelDescriptor PANEL_426_GDEQ0426T82 = {
 inline const EpdPanelDescriptor PANEL_426_GDEQ0426T82 = {
     .width = 800,
     .width = 800,
     .height = 480,
     .height = 480,
@@ -78,6 +84,8 @@ inline const EpdPanelDescriptor PANEL_426_GDEQ0426T82 = {
     .plane_invert = {false, false},
     .plane_invert = {false, false},
     .refresh_sequence = PANEL_426_GDEQ0426T82_REFRESH,
     .refresh_sequence = PANEL_426_GDEQ0426T82_REFRESH,
     .refresh_sequence_len = sizeof(PANEL_426_GDEQ0426T82_REFRESH) / sizeof(PANEL_426_GDEQ0426T82_REFRESH[0]),
     .refresh_sequence_len = sizeof(PANEL_426_GDEQ0426T82_REFRESH) / sizeof(PANEL_426_GDEQ0426T82_REFRESH[0]),
+    .hibernate_sequence = PANEL_426_GDEQ0426T82_HIBERNATE,
+    .hibernate_sequence_len = sizeof(PANEL_426_GDEQ0426T82_HIBERNATE) / sizeof(PANEL_426_GDEQ0426T82_HIBERNATE[0]),
 };
 };
 
 
 }  // namespace esphome::gxepd2
 }  // namespace esphome::gxepd2

+ 16 - 0
components/gxepd2/panel_579c_gdey0579f51.h

@@ -113,6 +113,18 @@ inline const EpdCommand PANEL_579C_GDEY0579F51_REFRESH[] = {
      /*wait_busy_after=*/true},
      /*wait_busy_after=*/true},
 };
 };
 
 
+inline const uint8_t PANEL_579C_GDEY0579F51_POWER_OFF_0_02[] = {0x00};
+
+inline const EpdCommand PANEL_579C_GDEY0579F51_POWER_OFF[] = {
+    {0x02, PANEL_579C_GDEY0579F51_POWER_OFF_0_02, 1, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},
+};
+
+inline const uint8_t PANEL_579C_GDEY0579F51_HIBERNATE_0_07[] = {0xA5};
+
+inline const EpdCommand PANEL_579C_GDEY0579F51_HIBERNATE[] = {
+    {0x07, PANEL_579C_GDEY0579F51_HIBERNATE_0_07, 1},
+};
+
 inline const EpdPanelDescriptor PANEL_579C_GDEY0579F51 = {
 inline const EpdPanelDescriptor PANEL_579C_GDEY0579F51 = {
     .width = 792,   // full logical panel - the canvas ESPHome draws into
     .width = 792,   // full logical panel - the canvas ESPHome draws into
     .height = 272,
     .height = 272,
@@ -148,6 +160,10 @@ inline const EpdPanelDescriptor PANEL_579C_GDEY0579F51 = {
     .blank_code = 0x01,
     .blank_code = 0x01,
     .refresh_sequence = PANEL_579C_GDEY0579F51_REFRESH,
     .refresh_sequence = PANEL_579C_GDEY0579F51_REFRESH,
     .refresh_sequence_len = sizeof(PANEL_579C_GDEY0579F51_REFRESH) / sizeof(PANEL_579C_GDEY0579F51_REFRESH[0]),
     .refresh_sequence_len = sizeof(PANEL_579C_GDEY0579F51_REFRESH) / sizeof(PANEL_579C_GDEY0579F51_REFRESH[0]),
+    .power_off_sequence = PANEL_579C_GDEY0579F51_POWER_OFF,
+    .power_off_sequence_len = sizeof(PANEL_579C_GDEY0579F51_POWER_OFF) / sizeof(PANEL_579C_GDEY0579F51_POWER_OFF[0]),
+    .hibernate_sequence = PANEL_579C_GDEY0579F51_HIBERNATE,
+    .hibernate_sequence_len = sizeof(PANEL_579C_GDEY0579F51_HIBERNATE) / sizeof(PANEL_579C_GDEY0579F51_HIBERNATE[0]),
 };
 };
 
 
 }  // namespace esphome::gxepd2
 }  // namespace esphome::gxepd2

+ 14 - 0
components/gxepd2/panel_583.h

@@ -76,6 +76,16 @@ inline const EpdCommand PANEL_583_REFRESH[] = {
     {0x12, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},
     {0x12, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},
 };
 };
 
 
+inline const EpdCommand PANEL_583_POWER_OFF[] = {
+    {0x02, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},
+};
+
+inline const uint8_t PANEL_583_HIBERNATE_0_07[] = {0xA5};
+
+inline const EpdCommand PANEL_583_HIBERNATE[] = {
+    {0x07, PANEL_583_HIBERNATE_0_07, 1},
+};
+
 inline const EpdPanelDescriptor PANEL_583 = {
 inline const EpdPanelDescriptor PANEL_583 = {
     .width = 600,
     .width = 600,
     .height = 448,
     .height = 448,
@@ -94,6 +104,10 @@ inline const EpdPanelDescriptor PANEL_583 = {
     .blank_code = 0x03,  // observed white-fill code
     .blank_code = 0x03,  // observed white-fill code
     .refresh_sequence = PANEL_583_REFRESH,
     .refresh_sequence = PANEL_583_REFRESH,
     .refresh_sequence_len = sizeof(PANEL_583_REFRESH) / sizeof(PANEL_583_REFRESH[0]),
     .refresh_sequence_len = sizeof(PANEL_583_REFRESH) / sizeof(PANEL_583_REFRESH[0]),
+    .power_off_sequence = PANEL_583_POWER_OFF,
+    .power_off_sequence_len = sizeof(PANEL_583_POWER_OFF) / sizeof(PANEL_583_POWER_OFF[0]),
+    .hibernate_sequence = PANEL_583_HIBERNATE,
+    .hibernate_sequence_len = sizeof(PANEL_583_HIBERNATE) / sizeof(PANEL_583_HIBERNATE[0]),
 };
 };
 
 
 }  // namespace esphome::gxepd2
 }  // namespace esphome::gxepd2

+ 14 - 0
components/gxepd2/panel_583_gdeq0583t31.h

@@ -78,6 +78,16 @@ inline const EpdCommand PANEL_583_GDEQ0583T31_REFRESH[] = {
     {0x12, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},  // display refresh
     {0x12, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},  // display refresh
 };
 };
 
 
+inline const EpdCommand PANEL_583_GDEQ0583T31_POWER_OFF[] = {
+    {0x02, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},
+};
+
+inline const uint8_t PANEL_583_GDEQ0583T31_HIBERNATE_0_07[] = {0xA5};
+
+inline const EpdCommand PANEL_583_GDEQ0583T31_HIBERNATE[] = {
+    {0x07, PANEL_583_GDEQ0583T31_HIBERNATE_0_07, 1},
+};
+
 inline const EpdPanelDescriptor PANEL_583_GDEQ0583T31 = {
 inline const EpdPanelDescriptor PANEL_583_GDEQ0583T31 = {
     .width = 648,
     .width = 648,
     .height = 480,
     .height = 480,
@@ -92,6 +102,10 @@ inline const EpdPanelDescriptor PANEL_583_GDEQ0583T31 = {
     .plane_invert = {false, false},
     .plane_invert = {false, false},
     .refresh_sequence = PANEL_583_GDEQ0583T31_REFRESH,
     .refresh_sequence = PANEL_583_GDEQ0583T31_REFRESH,
     .refresh_sequence_len = sizeof(PANEL_583_GDEQ0583T31_REFRESH) / sizeof(PANEL_583_GDEQ0583T31_REFRESH[0]),
     .refresh_sequence_len = sizeof(PANEL_583_GDEQ0583T31_REFRESH) / sizeof(PANEL_583_GDEQ0583T31_REFRESH[0]),
+    .power_off_sequence = PANEL_583_GDEQ0583T31_POWER_OFF,
+    .power_off_sequence_len = sizeof(PANEL_583_GDEQ0583T31_POWER_OFF) / sizeof(PANEL_583_GDEQ0583T31_POWER_OFF[0]),
+    .hibernate_sequence = PANEL_583_GDEQ0583T31_HIBERNATE,
+    .hibernate_sequence_len = sizeof(PANEL_583_GDEQ0583T31_HIBERNATE) / sizeof(PANEL_583_GDEQ0583T31_HIBERNATE[0]),
 };
 };
 
 
 }  // namespace esphome::gxepd2
 }  // namespace esphome::gxepd2

+ 16 - 0
components/gxepd2/panel_730c_gdep073e01.h

@@ -115,6 +115,18 @@ inline const EpdPaletteEntry PANEL_730C_GDEP073E01_PALETTE[] = {
 inline const uint8_t PANEL_730C_GDEP073E01_PALETTE_LEN =
 inline const uint8_t PANEL_730C_GDEP073E01_PALETTE_LEN =
     sizeof(PANEL_730C_GDEP073E01_PALETTE) / sizeof(PANEL_730C_GDEP073E01_PALETTE[0]);
     sizeof(PANEL_730C_GDEP073E01_PALETTE) / sizeof(PANEL_730C_GDEP073E01_PALETTE[0]);
 
 
+inline const uint8_t PANEL_730C_GDEP073E01_POWER_OFF_0_02[] = {0x00};
+
+inline const EpdCommand PANEL_730C_GDEP073E01_POWER_OFF[] = {
+    {0x02, PANEL_730C_GDEP073E01_POWER_OFF_0_02, 1, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},
+};
+
+inline const uint8_t PANEL_730C_GDEP073E01_HIBERNATE_0_07[] = {0xA5};
+
+inline const EpdCommand PANEL_730C_GDEP073E01_HIBERNATE[] = {
+    {0x07, PANEL_730C_GDEP073E01_HIBERNATE_0_07, 1},
+};
+
 inline const EpdPanelDescriptor PANEL_730C_GDEP073E01 = {
 inline const EpdPanelDescriptor PANEL_730C_GDEP073E01 = {
     .width = 800,
     .width = 800,
     .height = 480,
     .height = 480,
@@ -133,6 +145,10 @@ inline const EpdPanelDescriptor PANEL_730C_GDEP073E01 = {
     .blank_code = 0x01,
     .blank_code = 0x01,
     .refresh_sequence = PANEL_730C_GDEP073E01_REFRESH,
     .refresh_sequence = PANEL_730C_GDEP073E01_REFRESH,
     .refresh_sequence_len = sizeof(PANEL_730C_GDEP073E01_REFRESH) / sizeof(PANEL_730C_GDEP073E01_REFRESH[0]),
     .refresh_sequence_len = sizeof(PANEL_730C_GDEP073E01_REFRESH) / sizeof(PANEL_730C_GDEP073E01_REFRESH[0]),
+    .power_off_sequence = PANEL_730C_GDEP073E01_POWER_OFF,
+    .power_off_sequence_len = sizeof(PANEL_730C_GDEP073E01_POWER_OFF) / sizeof(PANEL_730C_GDEP073E01_POWER_OFF[0]),
+    .hibernate_sequence = PANEL_730C_GDEP073E01_HIBERNATE,
+    .hibernate_sequence_len = sizeof(PANEL_730C_GDEP073E01_HIBERNATE) / sizeof(PANEL_730C_GDEP073E01_HIBERNATE[0]),
 };
 };
 
 
 }  // namespace esphome::gxepd2
 }  // namespace esphome::gxepd2

+ 14 - 0
components/gxepd2/panel_750_gdey075t7.h

@@ -77,6 +77,16 @@ inline const EpdCommand PANEL_750_GDEY075T7_REFRESH[] = {
     {0x12, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},  // display refresh
     {0x12, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},  // display refresh
 };
 };
 
 
+inline const EpdCommand PANEL_750_GDEY075T7_POWER_OFF[] = {
+    {0x02, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},
+};
+
+inline const uint8_t PANEL_750_GDEY075T7_HIBERNATE_0_07[] = {0xA5};
+
+inline const EpdCommand PANEL_750_GDEY075T7_HIBERNATE[] = {
+    {0x07, PANEL_750_GDEY075T7_HIBERNATE_0_07, 1},
+};
+
 inline const EpdPanelDescriptor PANEL_750_GDEY075T7 = {
 inline const EpdPanelDescriptor PANEL_750_GDEY075T7 = {
     .width = 800,
     .width = 800,
     .height = 480,
     .height = 480,
@@ -92,6 +102,10 @@ inline const EpdPanelDescriptor PANEL_750_GDEY075T7 = {
     .bits_per_pixel = 1,
     .bits_per_pixel = 1,
     .refresh_sequence = PANEL_750_GDEY075T7_REFRESH,
     .refresh_sequence = PANEL_750_GDEY075T7_REFRESH,
     .refresh_sequence_len = sizeof(PANEL_750_GDEY075T7_REFRESH) / sizeof(PANEL_750_GDEY075T7_REFRESH[0]),
     .refresh_sequence_len = sizeof(PANEL_750_GDEY075T7_REFRESH) / sizeof(PANEL_750_GDEY075T7_REFRESH[0]),
+    .power_off_sequence = PANEL_750_GDEY075T7_POWER_OFF,
+    .power_off_sequence_len = sizeof(PANEL_750_GDEY075T7_POWER_OFF) / sizeof(PANEL_750_GDEY075T7_POWER_OFF[0]),
+    .hibernate_sequence = PANEL_750_GDEY075T7_HIBERNATE,
+    .hibernate_sequence_len = sizeof(PANEL_750_GDEY075T7_HIBERNATE) / sizeof(PANEL_750_GDEY075T7_HIBERNATE[0]),
 };
 };
 
 
 }  // namespace esphome::gxepd2
 }  // namespace esphome::gxepd2

+ 22 - 8
tools/capture.py

@@ -257,12 +257,23 @@ def run_program(model: str, catalog: dict, mode: str, cxx: str) -> str:
     hdr_rel = src_rel[:-4] + ".h"
     hdr_rel = src_rel[:-4] + ".h"
     if not (VSRC / hdr_rel).exists():
     if not (VSRC / hdr_rel).exists():
         sys.exit(f"driver header not found: {VSRC / hdr_rel}")
         sys.exit(f"driver header not found: {VSRC / hdr_rel}")
-    header, fn = _PROGRAMS[mode]
-    main_src = (
-        f'#include "{header}"\n'
-        f'#include "{hdr_rel}"\n'
-        f'int main() {{ {fn}<{real_class}>("{real_class}"); return 0; }}\n'
-    )
+    if mode == "power":
+        # emit_descriptor.h's print_power_sequences, namespaced and keyed by
+        # macro (not name) like emit_descriptor() itself - not run_harness.h/
+        # run_diag_harness.h's plain-global, name-keyed shape, so it doesn't
+        # fit _PROGRAMS below.
+        main_src = (
+            f'#include "emit_descriptor.h"\n'
+            f'#include "{hdr_rel}"\n'
+            f'int main() {{ emit::print_power_sequences<{real_class}>("{macro_of(model)}"); return 0; }}\n'
+        )
+    else:
+        header, fn = _PROGRAMS[mode]
+        main_src = (
+            f'#include "{header}"\n'
+            f'#include "{hdr_rel}"\n'
+            f'int main() {{ {fn}<{real_class}>("{real_class}"); return 0; }}\n'
+        )
     with tempfile.TemporaryDirectory() as td:
     with tempfile.TemporaryDirectory() as td:
         tdp = pathlib.Path(td)
         tdp = pathlib.Path(td)
         main_cpp = tdp / "main.cpp"
         main_cpp = tdp / "main.cpp"
@@ -295,6 +306,9 @@ def main() -> None:
                      help="run the window-encoder sweep (run_harness.h) instead of emitting a descriptor")
                      help="run the window-encoder sweep (run_harness.h) instead of emitting a descriptor")
     ap.add_argument("--diag", action="store_true",
     ap.add_argument("--diag", action="store_true",
                      help="run the Pattern-A row/byte-order diagnostic fill (run_diag_harness.h) instead of emitting")
                      help="run the Pattern-A row/byte-order diagnostic fill (run_diag_harness.h) instead of emitting")
+    ap.add_argument("--power", action="store_true",
+                     help="print just power_off_sequence/hibernate_sequence (for splicing into an existing "
+                          "panel_*.h), instead of emitting a full descriptor")
     ap.add_argument("--cxx", default="g++", help="C++ compiler (default: g++)")
     ap.add_argument("--cxx", default="g++", help="C++ compiler (default: g++)")
     args = ap.parse_args()
     args = ap.parse_args()
 
 
@@ -304,8 +318,8 @@ def main() -> None:
         return
         return
     if not args.model:
     if not args.model:
         ap.error("a model is required (or use --list)")
         ap.error("a model is required (or use --list)")
-    if args.sweep or args.diag:
-        mode = "sweep" if args.sweep else "diag"
+    if args.sweep or args.diag or args.power:
+        mode = "sweep" if args.sweep else "diag" if args.diag else "power"
         out = run_program(args.model, catalog, mode, args.cxx)
         out = run_program(args.model, catalog, mode, args.cxx)
         if args.out:
         if args.out:
             pathlib.Path(args.out).write_text(out)
             pathlib.Path(args.out).write_text(out)

+ 7 - 0
tools/gxepd2_harness/README.md

@@ -102,6 +102,13 @@ synthetic `MASTER_ONLY` variant) are skipped. Run it after any
   new. Output is the full byte-by-byte trace (large for a big/packed panel -
   new. Output is the full byte-by-byte trace (large for a big/packed panel -
   redirect to a file and grep for `>>> SWEEP` / `[row-tag]` rather than
   redirect to a file and grep for `>>> SWEEP` / `[row-tag]` rather than
   reading it raw).
   reading it raw).
+- `tools/capture.py MODEL --power` — prints just `power_off_sequence`/
+  `hibernate_sequence` (array + `EpdCommand` table + the two descriptor-field
+  lines to paste), for splicing into an EXISTING `panel_*.h` that shipped
+  before these fields existed, without regenerating - and losing any
+  hand-written commentary in - the rest of the file. Same noisy-trace caveat
+  as `--sweep`/`--diag`: redirect to a file and take the tail (everything
+  after the last `digitalRead` line is the actual result).
 - `run_<panel>_probe.cpp` + `probe_plane_invert.h` — print the `plane_invert`
 - `run_<panel>_probe.cpp` + `probe_plane_invert.h` — print the `plane_invert`
   probe result on its own.
   probe result on its own.
 - `run_*diag*.cpp` — the diagnostic-fill passes that derive pixel packing,
 - `run_*diag*.cpp` — the diagnostic-fill passes that derive pixel packing,

+ 126 - 0
tools/gxepd2_harness/emit_descriptor.h

@@ -335,6 +335,111 @@ template<typename Driver> PackedResolverFn make_resolver_bw_packed(uint8_t p0cmd
   };
   };
 }
 }
 
 
+// Captures powerOff() or hibernate() as its own standalone command sequence -
+// NOT folded into the refresh_seq phase-segmentation above, and NOT captured
+// from the same live session as each other. Every GxEPD2 driver's powerOff()
+// guards on `_power_is_on` (set true by the refresh() a normal update ends
+// with), and hibernate() calls powerOff() again internally - so capturing
+// both from one session would have the second call's powerOff() portion
+// silently emit nothing (already off from the first). Each call here gets its
+// own fresh instance, replayed through the exact init/clear/write/refresh
+// setup the main capture above uses, so `call` always runs with
+// _power_is_on true, matching a real update-then-power-down.
+template<typename Driver, typename MethodCall> std::vector<Cmd> capture_power_sequence(MethodCall call) {
+  constexpr int16_t CS = 10, DC = 11, RST = 12, BUSY = 13;
+  Trace::set_dc_pin(DC);
+  Peek<Driver> d(CS, DC, RST, BUSY);
+  Trace::last_busy_release_value = d.busy_level() == HIGH ? LOW : HIGH;
+  d.selectSPI(SPI, SPISettings());
+  d.init(0);
+
+  const uint32_t plane_bytes = (uint32_t) Driver::WIDTH * Driver::HEIGHT / 8;
+  auto *buf = new uint8_t[plane_bytes];
+  memset(buf, 0xA5, plane_bytes);
+  d.writeScreenBuffer();
+  d.writeImage(buf, buf, 0, 0, Driver::WIDTH, Driver::HEIGHT);
+  d.refresh(false);
+  delete[] buf;
+
+  Trace::start_event_record();
+  call(d);
+  Trace::stop_event_record();
+  return group(0, Trace::events.size());
+}
+
+// (power_off_sequence, hibernate_sequence) for Driver, each captured via
+// capture_power_sequence above. hibernate_sequence is hibernate()'s tail
+// PAST whatever it shares with powerOff() - every driver observed so far
+// calls powerOff() internally then adds exactly one more command (the
+// deep-sleep trigger) - since power_off_sequence already has that shared
+// part and CycleEngine runs the two back to back (see epd_command.h); a
+// prefix mismatch (seen nowhere so far) falls back to the full hibernate
+// capture rather than guess.
+template<typename Driver> std::pair<std::vector<Cmd>, std::vector<Cmd>> capture_power_off_and_hibernate() {
+  std::vector<Cmd> power_off_seq = capture_power_sequence<Driver>([](Peek<Driver> &d) { d.powerOff(); });
+  std::vector<Cmd> hibernate_full_seq = capture_power_sequence<Driver>([](Peek<Driver> &d) { d.hibernate(); });
+  std::vector<Cmd> hibernate_seq;
+  if (hibernate_full_seq.size() > power_off_seq.size()) {
+    bool prefix_matches = true;
+    for (size_t i = 0; i < power_off_seq.size() && prefix_matches; i++)
+      if (hibernate_full_seq[i].cmd != power_off_seq[i].cmd || hibernate_full_seq[i].data != power_off_seq[i].data)
+        prefix_matches = false;
+    if (prefix_matches)
+      hibernate_seq.assign(hibernate_full_seq.begin() + power_off_seq.size(), hibernate_full_seq.end());
+  }
+  if (hibernate_seq.empty())
+    hibernate_seq = hibernate_full_seq;
+  return {power_off_seq, hibernate_seq};
+}
+
+// Prints just the POWER_OFF/HIBERNATE arrays + EpdCommand tables (plus the
+// descriptor-field lines to paste) for Driver - for splicing
+// power_off_sequence/hibernate_sequence into an EXISTING, hand-maintained
+// panel_*.h (one shipped before these fields existed) without regenerating,
+// and losing, everything else already in it. tools/capture.py --power.
+template<typename Driver> void print_power_sequences(const char *macro) {
+  auto captured = capture_power_off_and_hibernate<Driver>();
+  const std::vector<Cmd> &power_off_seq = captured.first;
+  const std::vector<Cmd> &hibernate_seq = captured.second;
+
+  auto emit_seq = [&](const char *seqsuffix, const std::vector<Cmd> &cmds) {
+    for (size_t i = 0; i < cmds.size(); i++)
+      if (!cmds[i].data.empty()) {
+        char suf[64];
+        snprintf(suf, sizeof(suf), "%s_%zu_%02X", seqsuffix, i, cmds[i].cmd);
+        print_data_array(macro, suf, cmds[i].data);
+      }
+    printf("\ninline const EpdCommand %s_%s[] = {\n", macro, seqsuffix);
+    for (size_t i = 0; i < cmds.size(); i++) {
+      char suf[64];
+      snprintf(suf, sizeof(suf), "%s_%zu_%02X", seqsuffix, i, cmds[i].cmd);
+      print_cmd(macro, suf, cmds[i]);
+    }
+    printf("};\n");
+  };
+
+  if (!power_off_seq.empty()) {
+    emit_seq("POWER_OFF", power_off_seq);
+    printf("// splice into the descriptor:\n");
+    printf("//   .power_off_sequence = %s_POWER_OFF,\n", macro);
+    printf("//   .power_off_sequence_len = sizeof(%s_POWER_OFF) / sizeof(%s_POWER_OFF[0]),\n", macro, macro);
+  } else {
+    printf("// powerOff(): empty capture - this driver's full refresh already leaves\n"
+           "// _power_is_on false (its _Update_Full() powers itself down), so powerOff()\n"
+           "// has nothing left to do. No power_off_sequence to add.\n");
+  }
+  printf("\n");
+  if (!hibernate_seq.empty()) {
+    emit_seq("HIBERNATE", hibernate_seq);
+    printf("// splice into the descriptor:\n");
+    printf("//   .hibernate_sequence = %s_HIBERNATE,\n", macro);
+    printf("//   .hibernate_sequence_len = sizeof(%s_HIBERNATE) / sizeof(%s_HIBERNATE[0]),\n", macro, macro);
+  } else {
+    printf("// hibernate(): empty capture (unexpected - every driver seen so far adds at\n"
+           "// least the deep-sleep trigger when rst is wired). Review by hand.\n");
+  }
+}
+
 template<typename Driver>
 template<typename Driver>
 void emit_descriptor_impl(const char *class_name, const char *macro, const PackedResolverFn &resolve_packed) {
 void emit_descriptor_impl(const char *class_name, const char *macro, const PackedResolverFn &resolve_packed) {
   constexpr int16_t CS = 10, DC = 11, RST = 12, BUSY = 13;
   constexpr int16_t CS = 10, DC = 11, RST = 12, BUSY = 13;
@@ -594,6 +699,12 @@ void emit_descriptor_impl(const char *class_name, const char *macro, const Packe
       refresh_seq.resize(last_wait + 1);
       refresh_seq.resize(last_wait + 1);
   }
   }
 
 
+  // powerOff()/hibernate(), each its own fresh-session capture - see
+  // capture_power_off_and_hibernate's doc comment.
+  auto power_captured = capture_power_off_and_hibernate<Driver>();
+  std::vector<Cmd> &power_off_seq = power_captured.first;
+  std::vector<Cmd> &hibernate_seq = power_captured.second;
+
   // --- emit ---
   // --- emit ---
   printf("// GENERATED by emit_descriptor from %s - candidate.\n", class_name);
   printf("// GENERATED by emit_descriptor from %s - candidate.\n", class_name);
   printf("// Command sequences, plane layout and plane_invert are observed here;\n");
   printf("// Command sequences, plane layout and plane_invert are observed here;\n");
@@ -637,6 +748,12 @@ void emit_descriptor_impl(const char *class_name, const char *macro, const Packe
   if (per_plane)
   if (per_plane)
     emit_seq("WINDOW_SEQ1", window1);
     emit_seq("WINDOW_SEQ1", window1);
   emit_seq("REFRESH", refresh_seq);
   emit_seq("REFRESH", refresh_seq);
+  const bool has_power_off = !power_off_seq.empty();
+  const bool has_hibernate = !hibernate_seq.empty();
+  if (has_power_off)
+    emit_seq("POWER_OFF", power_off_seq);
+  if (has_hibernate)
+    emit_seq("HIBERNATE", hibernate_seq);
 
 
   printf("inline const EpdPanelDescriptor %s = {\n", macro);
   printf("inline const EpdPanelDescriptor %s = {\n", macro);
   printf("    .width = %u,\n", (unsigned) Driver::WIDTH);
   printf("    .width = %u,\n", (unsigned) Driver::WIDTH);
@@ -730,6 +847,15 @@ void emit_descriptor_impl(const char *class_name, const char *macro, const Packe
   }
   }
   printf("    .refresh_sequence = %s_REFRESH,\n", macro);
   printf("    .refresh_sequence = %s_REFRESH,\n", macro);
   printf("    .refresh_sequence_len = sizeof(%s_REFRESH) / sizeof(%s_REFRESH[0]),\n", macro, macro);
   printf("    .refresh_sequence_len = sizeof(%s_REFRESH) / sizeof(%s_REFRESH[0]),\n", macro, macro);
+  if (has_power_off) {
+    printf("    .power_off_sequence = %s_POWER_OFF,  // probe: powerOff(), fresh session after a normal refresh\n", macro);
+    printf("    .power_off_sequence_len = sizeof(%s_POWER_OFF) / sizeof(%s_POWER_OFF[0]),\n", macro, macro);
+  }
+  if (has_hibernate) {
+    printf("    .hibernate_sequence = %s_HIBERNATE,  // probe: hibernate()'s tail past powerOff() (see file header)\n",
+           macro);
+    printf("    .hibernate_sequence_len = sizeof(%s_HIBERNATE) / sizeof(%s_HIBERNATE[0]),\n", macro, macro);
+  }
   printf("};\n\n}  // namespace esphome::gxepd2\n");
   printf("};\n\n}  // namespace esphome::gxepd2\n");
 }
 }