Explorar o código

Capture writeImageForFullRefresh's extra RAM-bank write for partial refresh

GxEPD2's writeImageForFullRefresh() diverges from plain writeImage() on
SSD1680/SSD1681/SSD1619-family controllers: it writes the same bitmap to a
"previous RAM" bank before the normal "current RAM" write, so a later
partial update's internal diff starts from a correct baseline. This engine
is full-refresh-only today (that waveform doesn't read the previous bank),
so it costs nothing now, but it's exactly the state partial refresh would
need - captured now via a generic emit_descriptor probe rather than needing
a second capture pass later.

New EpdPanelDescriptor::plane_write_cmd_for_full_refresh field (default
{0,0}, not read by CycleEngine yet) plus a capture.py --full-refresh splice
mode. Swept all 61 shipped mono/3-color panels: 27 genuinely diverge: 25
plain auto-generated descriptors regenerated wholesale, 2 with hand-authored
documentation (panel_426_gdeq0426t82.h, panel_583_gdeq0583t31.h) spliced by
hand to preserve it. check_descriptors.py reports all 61 panels unchanged.
ZiB hai 3 semanas
pai
achega
187fa75490

+ 16 - 0
components/gxepd2/epd_command.h

@@ -119,6 +119,22 @@ struct EpdPanelDescriptor {
 
 
   uint8_t plane_count;          // 1 (BW), 2 (BW + accent color), or 2 physical regions
   uint8_t plane_count;          // 1 (BW), 2 (BW + accent color), or 2 physical regions
   uint8_t plane_write_cmd[2];   // e.g. {0x24, 0x26}
   uint8_t plane_write_cmd[2];   // e.g. {0x24, 0x26}
+
+  // The extra "previous RAM bank" write GxEPD2's writeImageForFullRefresh()
+  // makes that plain writeImage() doesn't, on controllers where the two
+  // diverge (SSD1680/SSD1681/SSD1619-family: 0x26 then 0x24, or 0x10 then
+  // 0x13 - same bitmap written twice, to two different commands). 0x00 (the
+  // default, and every panel captured before this field existed) means no
+  // divergence was observed: writeImageForFullRefresh falls back to
+  // writeImage for that controller, the common case. NOT read by CycleEngine
+  // today - this engine always does a plain single write per plane, correct
+  // for full-refresh-only operation, since the full-refresh waveform doesn't
+  // depend on the "previous" bank's content. It matters once partial refresh
+  // reads that bank to compute its diff, so it's captured now rather than
+  // needing a second capture pass later - see emit_descriptor.h's
+  // probe_full_refresh_extra_write_cmd.
+  uint8_t plane_write_cmd_for_full_refresh[2]{0, 0};
+
   bool plane_invert[2];         // true if the byte must be bit-inverted on write
   bool plane_invert[2];         // true if the byte must be bit-inverted on write
 
 
   // When window_per_plane is true, each plane's own buffer is sized for
   // When window_per_plane is true, each plane's own buffer is sized for

+ 1 - 0
components/gxepd2/panel_102.h

@@ -109,6 +109,7 @@ inline const EpdPanelDescriptor PANEL_102 = {
     .window_sequence_len = {sizeof(PANEL_102_WINDOW_SEQ0) / sizeof(PANEL_102_WINDOW_SEQ0[0]), 0},
     .window_sequence_len = {sizeof(PANEL_102_WINDOW_SEQ0) / sizeof(PANEL_102_WINDOW_SEQ0[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x13, 0x00},
     .plane_write_cmd = {0x13, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x10, 0x00},  // writeImageForFullRefresh also re-writes this "previous RAM" bank before the 0x13 write above - not read by this full-refresh-only engine yet, captured for future partial-refresh use
     .plane_invert = {false, false},  // probe-derived
     .plane_invert = {false, false},  // probe-derived
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .palette = nullptr,
     .palette = nullptr,

+ 1 - 0
components/gxepd2/panel_1020_gdem102t91.h

@@ -60,6 +60,7 @@ inline const EpdPanelDescriptor PANEL_1020_GDEM102T91 = {
     .window_sequence_len = {sizeof(PANEL_1020_GDEM102T91_WINDOW_SEQ0) / sizeof(PANEL_1020_GDEM102T91_WINDOW_SEQ0[0]), 0},
     .window_sequence_len = {sizeof(PANEL_1020_GDEM102T91_WINDOW_SEQ0) / sizeof(PANEL_1020_GDEM102T91_WINDOW_SEQ0[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x24, 0x00},
     .plane_write_cmd = {0x24, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x26, 0x00},  // writeImageForFullRefresh also re-writes this "previous RAM" bank before the 0x24 write above - not read by this full-refresh-only engine yet, captured for future partial-refresh use
     .plane_invert = {false, false},  // probe-derived
     .plane_invert = {false, false},  // probe-derived
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .palette = nullptr,
     .palette = nullptr,

+ 1 - 0
components/gxepd2/panel_1160_t91.h

@@ -70,6 +70,7 @@ inline const EpdPanelDescriptor PANEL_1160_T91 = {
     .window_sequence_len = {sizeof(PANEL_1160_T91_WINDOW_SEQ0) / sizeof(PANEL_1160_T91_WINDOW_SEQ0[0]), 0},
     .window_sequence_len = {sizeof(PANEL_1160_T91_WINDOW_SEQ0) / sizeof(PANEL_1160_T91_WINDOW_SEQ0[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x24, 0x00},
     .plane_write_cmd = {0x24, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x26, 0x00},  // writeImageForFullRefresh also re-writes this "previous RAM" bank before the 0x24 write above - not read by this full-refresh-only engine yet, captured for future partial-refresh use
     .plane_invert = {false, false},  // probe-derived
     .plane_invert = {false, false},  // probe-derived
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .palette = nullptr,
     .palette = nullptr,

+ 1 - 0
components/gxepd2/panel_1330_gdem133t91.h

@@ -60,6 +60,7 @@ inline const EpdPanelDescriptor PANEL_1330_GDEM133T91 = {
     .window_sequence_len = {sizeof(PANEL_1330_GDEM133T91_WINDOW_SEQ0) / sizeof(PANEL_1330_GDEM133T91_WINDOW_SEQ0[0]), 0},
     .window_sequence_len = {sizeof(PANEL_1330_GDEM133T91_WINDOW_SEQ0) / sizeof(PANEL_1330_GDEM133T91_WINDOW_SEQ0[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x24, 0x00},
     .plane_write_cmd = {0x24, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x26, 0x00},  // writeImageForFullRefresh also re-writes this "previous RAM" bank before the 0x24 write above - not read by this full-refresh-only engine yet, captured for future partial-refresh use
     .plane_invert = {false, false},  // probe-derived
     .plane_invert = {false, false},  // probe-derived
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .palette = nullptr,
     .palette = nullptr,

+ 1 - 0
components/gxepd2/panel_150_bn.h

@@ -61,6 +61,7 @@ inline const EpdPanelDescriptor PANEL_150_BN = {
     .window_sequence_len = {sizeof(PANEL_150_BN_WINDOW_SEQ0) / sizeof(PANEL_150_BN_WINDOW_SEQ0[0]), 0},
     .window_sequence_len = {sizeof(PANEL_150_BN_WINDOW_SEQ0) / sizeof(PANEL_150_BN_WINDOW_SEQ0[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x24, 0x00},
     .plane_write_cmd = {0x24, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x26, 0x00},  // writeImageForFullRefresh also re-writes this "previous RAM" bank before the 0x24 write above - not read by this full-refresh-only engine yet, captured for future partial-refresh use
     .plane_invert = {false, false},  // probe-derived
     .plane_invert = {false, false},  // probe-derived
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .palette = nullptr,
     .palette = nullptr,

+ 1 - 0
components/gxepd2/panel_154_d67.h

@@ -58,6 +58,7 @@ inline const EpdPanelDescriptor PANEL_154_D67 = {
     .window_sequence_len = {sizeof(PANEL_154_D67_WINDOW_SEQ0) / sizeof(PANEL_154_D67_WINDOW_SEQ0[0]), 0},
     .window_sequence_len = {sizeof(PANEL_154_D67_WINDOW_SEQ0) / sizeof(PANEL_154_D67_WINDOW_SEQ0[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x24, 0x00},
     .plane_write_cmd = {0x24, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x26, 0x00},  // writeImageForFullRefresh also re-writes this "previous RAM" bank before the 0x24 write above - not read by this full-refresh-only engine yet, captured for future partial-refresh use
     .plane_invert = {false, false},  // probe-derived
     .plane_invert = {false, false},  // probe-derived
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .palette = nullptr,
     .palette = nullptr,

+ 1 - 0
components/gxepd2/panel_154_gdey0154d67.h

@@ -60,6 +60,7 @@ inline const EpdPanelDescriptor PANEL_154_GDEY0154D67 = {
     .window_sequence_len = {sizeof(PANEL_154_GDEY0154D67_WINDOW_SEQ0) / sizeof(PANEL_154_GDEY0154D67_WINDOW_SEQ0[0]), 0},
     .window_sequence_len = {sizeof(PANEL_154_GDEY0154D67_WINDOW_SEQ0) / sizeof(PANEL_154_GDEY0154D67_WINDOW_SEQ0[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x24, 0x00},
     .plane_write_cmd = {0x24, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x26, 0x00},  // writeImageForFullRefresh also re-writes this "previous RAM" bank before the 0x24 write above - not read by this full-refresh-only engine yet, captured for future partial-refresh use
     .plane_invert = {false, false},  // probe-derived
     .plane_invert = {false, false},  // probe-derived
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .palette = nullptr,
     .palette = nullptr,

+ 1 - 0
components/gxepd2/panel_213_b74.h

@@ -60,6 +60,7 @@ inline const EpdPanelDescriptor PANEL_213_B74 = {
     .window_sequence_len = {sizeof(PANEL_213_B74_WINDOW_SEQ0) / sizeof(PANEL_213_B74_WINDOW_SEQ0[0]), 0},
     .window_sequence_len = {sizeof(PANEL_213_B74_WINDOW_SEQ0) / sizeof(PANEL_213_B74_WINDOW_SEQ0[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x24, 0x00},
     .plane_write_cmd = {0x24, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x26, 0x00},  // writeImageForFullRefresh also re-writes this "previous RAM" bank before the 0x24 write above - not read by this full-refresh-only engine yet, captured for future partial-refresh use
     .plane_invert = {false, false},  // probe-derived
     .plane_invert = {false, false},  // probe-derived
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .palette = nullptr,
     .palette = nullptr,

+ 1 - 0
components/gxepd2/panel_213_bn.h

@@ -61,6 +61,7 @@ inline const EpdPanelDescriptor PANEL_213_BN = {
     .window_sequence_len = {sizeof(PANEL_213_BN_WINDOW_SEQ0) / sizeof(PANEL_213_BN_WINDOW_SEQ0[0]), 0},
     .window_sequence_len = {sizeof(PANEL_213_BN_WINDOW_SEQ0) / sizeof(PANEL_213_BN_WINDOW_SEQ0[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x24, 0x00},
     .plane_write_cmd = {0x24, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x26, 0x00},  // writeImageForFullRefresh also re-writes this "previous RAM" bank before the 0x24 write above - not read by this full-refresh-only engine yet, captured for future partial-refresh use
     .plane_invert = {false, false},  // probe-derived
     .plane_invert = {false, false},  // probe-derived
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .palette = nullptr,
     .palette = nullptr,

+ 1 - 0
components/gxepd2/panel_213_gdey0213b74.h

@@ -56,6 +56,7 @@ inline const EpdPanelDescriptor PANEL_213_GDEY0213B74 = {
     .window_sequence_len = {sizeof(PANEL_213_GDEY0213B74_WINDOW_SEQ0) / sizeof(PANEL_213_GDEY0213B74_WINDOW_SEQ0[0]), 0},
     .window_sequence_len = {sizeof(PANEL_213_GDEY0213B74_WINDOW_SEQ0) / sizeof(PANEL_213_GDEY0213B74_WINDOW_SEQ0[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x24, 0x00},
     .plane_write_cmd = {0x24, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x26, 0x00},  // writeImageForFullRefresh also re-writes this "previous RAM" bank before the 0x24 write above - not read by this full-refresh-only engine yet, captured for future partial-refresh use
     .plane_invert = {false, false},  // probe-derived
     .plane_invert = {false, false},  // probe-derived
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .palette = nullptr,
     .palette = nullptr,

+ 1 - 0
components/gxepd2/panel_213_t5d.h

@@ -77,6 +77,7 @@ inline const EpdPanelDescriptor PANEL_213_T5D = {
     .window_sequence_len = {sizeof(PANEL_213_T5D_WINDOW_SEQ0) / sizeof(PANEL_213_T5D_WINDOW_SEQ0[0]), 0},
     .window_sequence_len = {sizeof(PANEL_213_T5D_WINDOW_SEQ0) / sizeof(PANEL_213_T5D_WINDOW_SEQ0[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x13, 0x00},
     .plane_write_cmd = {0x13, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x10, 0x00},  // writeImageForFullRefresh also re-writes this "previous RAM" bank before the 0x13 write above - not read by this full-refresh-only engine yet, captured for future partial-refresh use
     .plane_invert = {false, false},  // probe-derived
     .plane_invert = {false, false},  // probe-derived
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .palette = nullptr,
     .palette = nullptr,

+ 1 - 0
components/gxepd2/panel_266_bn.h

@@ -61,6 +61,7 @@ inline const EpdPanelDescriptor PANEL_266_BN = {
     .window_sequence_len = {sizeof(PANEL_266_BN_WINDOW_SEQ0) / sizeof(PANEL_266_BN_WINDOW_SEQ0[0]), 0},
     .window_sequence_len = {sizeof(PANEL_266_BN_WINDOW_SEQ0) / sizeof(PANEL_266_BN_WINDOW_SEQ0[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x24, 0x00},
     .plane_write_cmd = {0x24, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x26, 0x00},  // writeImageForFullRefresh also re-writes this "previous RAM" bank before the 0x24 write above - not read by this full-refresh-only engine yet, captured for future partial-refresh use
     .plane_invert = {false, false},  // probe-derived
     .plane_invert = {false, false},  // probe-derived
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .palette = nullptr,
     .palette = nullptr,

+ 1 - 0
components/gxepd2/panel_270_gdey027t91.h

@@ -54,6 +54,7 @@ inline const EpdPanelDescriptor PANEL_270_GDEY027T91 = {
     .window_sequence_len = {sizeof(PANEL_270_GDEY027T91_WINDOW_SEQ0) / sizeof(PANEL_270_GDEY027T91_WINDOW_SEQ0[0]), 0},
     .window_sequence_len = {sizeof(PANEL_270_GDEY027T91_WINDOW_SEQ0) / sizeof(PANEL_270_GDEY027T91_WINDOW_SEQ0[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x24, 0x00},
     .plane_write_cmd = {0x24, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x26, 0x00},  // writeImageForFullRefresh also re-writes this "previous RAM" bank before the 0x24 write above - not read by this full-refresh-only engine yet, captured for future partial-refresh use
     .plane_invert = {false, false},  // probe-derived
     .plane_invert = {false, false},  // probe-derived
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .palette = nullptr,
     .palette = nullptr,

+ 1 - 0
components/gxepd2/panel_290_bs.h

@@ -54,6 +54,7 @@ inline const EpdPanelDescriptor PANEL_290_BS = {
     .window_sequence_len = {sizeof(PANEL_290_BS_WINDOW_SEQ0) / sizeof(PANEL_290_BS_WINDOW_SEQ0[0]), 0},
     .window_sequence_len = {sizeof(PANEL_290_BS_WINDOW_SEQ0) / sizeof(PANEL_290_BS_WINDOW_SEQ0[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x24, 0x00},
     .plane_write_cmd = {0x24, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x26, 0x00},  // writeImageForFullRefresh also re-writes this "previous RAM" bank before the 0x24 write above - not read by this full-refresh-only engine yet, captured for future partial-refresh use
     .plane_invert = {false, false},  // probe-derived
     .plane_invert = {false, false},  // probe-derived
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .palette = nullptr,
     .palette = nullptr,

+ 1 - 0
components/gxepd2/panel_290_gdey029t71h.h

@@ -67,6 +67,7 @@ inline const EpdPanelDescriptor PANEL_290_GDEY029T71H = {
     .window_sequence_len = {sizeof(PANEL_290_GDEY029T71H_WINDOW_SEQ0) / sizeof(PANEL_290_GDEY029T71H_WINDOW_SEQ0[0]), 0},
     .window_sequence_len = {sizeof(PANEL_290_GDEY029T71H_WINDOW_SEQ0) / sizeof(PANEL_290_GDEY029T71H_WINDOW_SEQ0[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x24, 0x00},
     .plane_write_cmd = {0x24, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x26, 0x00},  // writeImageForFullRefresh also re-writes this "previous RAM" bank before the 0x24 write above - not read by this full-refresh-only engine yet, captured for future partial-refresh use
     .plane_invert = {false, false},  // probe-derived
     .plane_invert = {false, false},  // probe-derived
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .palette = nullptr,
     .palette = nullptr,

+ 1 - 0
components/gxepd2/panel_290_gdey029t94.h

@@ -62,6 +62,7 @@ inline const EpdPanelDescriptor PANEL_290_GDEY029T94 = {
     .window_sequence_len = {sizeof(PANEL_290_GDEY029T94_WINDOW_SEQ0) / sizeof(PANEL_290_GDEY029T94_WINDOW_SEQ0[0]), 0},
     .window_sequence_len = {sizeof(PANEL_290_GDEY029T94_WINDOW_SEQ0) / sizeof(PANEL_290_GDEY029T94_WINDOW_SEQ0[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x24, 0x00},
     .plane_write_cmd = {0x24, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x26, 0x00},  // writeImageForFullRefresh also re-writes this "previous RAM" bank before the 0x24 write above - not read by this full-refresh-only engine yet, captured for future partial-refresh use
     .plane_invert = {false, false},  // probe-derived
     .plane_invert = {false, false},  // probe-derived
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .palette = nullptr,
     .palette = nullptr,

+ 1 - 0
components/gxepd2/panel_290_i6fd.h

@@ -77,6 +77,7 @@ inline const EpdPanelDescriptor PANEL_290_I6FD = {
     .window_sequence_len = {sizeof(PANEL_290_I6FD_WINDOW_SEQ0) / sizeof(PANEL_290_I6FD_WINDOW_SEQ0[0]), 0},
     .window_sequence_len = {sizeof(PANEL_290_I6FD_WINDOW_SEQ0) / sizeof(PANEL_290_I6FD_WINDOW_SEQ0[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x13, 0x00},
     .plane_write_cmd = {0x13, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x10, 0x00},  // writeImageForFullRefresh also re-writes this "previous RAM" bank before the 0x13 write above - not read by this full-refresh-only engine yet, captured for future partial-refresh use
     .plane_invert = {false, false},  // probe-derived
     .plane_invert = {false, false},  // probe-derived
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .palette = nullptr,
     .palette = nullptr,

+ 4 - 2
components/gxepd2/panel_290_t5d.h

@@ -55,6 +55,7 @@ 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[] = {
 inline const EpdCommand PANEL_290_T5D_POWER_OFF[] = {
     {0x02, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},
     {0x02, nullptr, 0, /*delay_before_ms=*/0, /*delay_after_ms=*/0, /*wait_busy_after=*/true},
 };
 };
@@ -76,15 +77,16 @@ inline const EpdPanelDescriptor PANEL_290_T5D = {
     .window_sequence_len = {sizeof(PANEL_290_T5D_WINDOW_SEQ0) / sizeof(PANEL_290_T5D_WINDOW_SEQ0[0]), 0},
     .window_sequence_len = {sizeof(PANEL_290_T5D_WINDOW_SEQ0) / sizeof(PANEL_290_T5D_WINDOW_SEQ0[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x13, 0x00},
     .plane_write_cmd = {0x13, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x10, 0x00},  // writeImageForFullRefresh also re-writes this "previous RAM" bank before the 0x13 write above - not read by this full-refresh-only engine yet, captured for future partial-refresh use
     .plane_invert = {false, false},  // probe-derived
     .plane_invert = {false, false},  // probe-derived
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .palette = nullptr,
     .palette = nullptr,
     .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 = PANEL_290_T5D_POWER_OFF,  // probe: powerOff(), fresh session after a normal refresh
     .power_off_sequence_len = sizeof(PANEL_290_T5D_POWER_OFF) / sizeof(PANEL_290_T5D_POWER_OFF[0]),
     .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 = PANEL_290_T5D_HIBERNATE,  // probe: hibernate()'s tail past powerOff() (see file header)
     .hibernate_sequence_len = sizeof(PANEL_290_T5D_HIBERNATE) / sizeof(PANEL_290_T5D_HIBERNATE[0]),
     .hibernate_sequence_len = sizeof(PANEL_290_T5D_HIBERNATE) / sizeof(PANEL_290_T5D_HIBERNATE[0]),
 };
 };
 
 

+ 1 - 0
components/gxepd2/panel_290_t94.h

@@ -54,6 +54,7 @@ inline const EpdPanelDescriptor PANEL_290_T94 = {
     .window_sequence_len = {sizeof(PANEL_290_T94_WINDOW_SEQ0) / sizeof(PANEL_290_T94_WINDOW_SEQ0[0]), 0},
     .window_sequence_len = {sizeof(PANEL_290_T94_WINDOW_SEQ0) / sizeof(PANEL_290_T94_WINDOW_SEQ0[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x24, 0x00},
     .plane_write_cmd = {0x24, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x26, 0x00},  // writeImageForFullRefresh also re-writes this "previous RAM" bank before the 0x24 write above - not read by this full-refresh-only engine yet, captured for future partial-refresh use
     .plane_invert = {false, false},  // probe-derived
     .plane_invert = {false, false},  // probe-derived
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .palette = nullptr,
     .palette = nullptr,

+ 1 - 0
components/gxepd2/panel_290_t94_v2.h

@@ -54,6 +54,7 @@ inline const EpdPanelDescriptor PANEL_290_T94_V2 = {
     .window_sequence_len = {sizeof(PANEL_290_T94_V2_WINDOW_SEQ0) / sizeof(PANEL_290_T94_V2_WINDOW_SEQ0[0]), 0},
     .window_sequence_len = {sizeof(PANEL_290_T94_V2_WINDOW_SEQ0) / sizeof(PANEL_290_T94_V2_WINDOW_SEQ0[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x24, 0x00},
     .plane_write_cmd = {0x24, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x26, 0x00},  // writeImageForFullRefresh also re-writes this "previous RAM" bank before the 0x24 write above - not read by this full-refresh-only engine yet, captured for future partial-refresh use
     .plane_invert = {false, false},  // probe-derived
     .plane_invert = {false, false},  // probe-derived
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .palette = nullptr,
     .palette = nullptr,

+ 1 - 0
components/gxepd2/panel_310_gdeq031t10.h

@@ -59,6 +59,7 @@ inline const EpdPanelDescriptor PANEL_310_GDEQ031T10 = {
     .window_sequence_len = {sizeof(PANEL_310_GDEQ031T10_WINDOW_SEQ0) / sizeof(PANEL_310_GDEQ031T10_WINDOW_SEQ0[0]), 0},
     .window_sequence_len = {sizeof(PANEL_310_GDEQ031T10_WINDOW_SEQ0) / sizeof(PANEL_310_GDEQ031T10_WINDOW_SEQ0[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x13, 0x00},
     .plane_write_cmd = {0x13, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x10, 0x00},  // writeImageForFullRefresh also re-writes this "previous RAM" bank before the 0x13 write above - not read by this full-refresh-only engine yet, captured for future partial-refresh use
     .plane_invert = {false, false},  // probe-derived
     .plane_invert = {false, false},  // probe-derived
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .palette = nullptr,
     .palette = nullptr,

+ 1 - 0
components/gxepd2/panel_370_tc1.h

@@ -61,6 +61,7 @@ inline const EpdPanelDescriptor PANEL_370_TC1 = {
     .window_sequence_len = {sizeof(PANEL_370_TC1_WINDOW_SEQ0) / sizeof(PANEL_370_TC1_WINDOW_SEQ0[0]), 0},
     .window_sequence_len = {sizeof(PANEL_370_TC1_WINDOW_SEQ0) / sizeof(PANEL_370_TC1_WINDOW_SEQ0[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x24, 0x00},
     .plane_write_cmd = {0x24, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x26, 0x00},  // writeImageForFullRefresh also re-writes this "previous RAM" bank before the 0x24 write above - not read by this full-refresh-only engine yet, captured for future partial-refresh use
     .plane_invert = {false, false},  // probe-derived
     .plane_invert = {false, false},  // probe-derived
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .palette = nullptr,
     .palette = nullptr,

+ 1 - 0
components/gxepd2/panel_397_gdem0397t81.h

@@ -64,6 +64,7 @@ inline const EpdPanelDescriptor PANEL_397_GDEM0397T81 = {
     .window_sequence_len = {sizeof(PANEL_397_GDEM0397T81_WINDOW_SEQ0) / sizeof(PANEL_397_GDEM0397T81_WINDOW_SEQ0[0]), 0},
     .window_sequence_len = {sizeof(PANEL_397_GDEM0397T81_WINDOW_SEQ0) / sizeof(PANEL_397_GDEM0397T81_WINDOW_SEQ0[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x24, 0x00},
     .plane_write_cmd = {0x24, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x26, 0x00},  // writeImageForFullRefresh also re-writes this "previous RAM" bank before the 0x24 write above - not read by this full-refresh-only engine yet, captured for future partial-refresh use
     .plane_invert = {false, false},  // probe-derived
     .plane_invert = {false, false},  // probe-derived
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .palette = nullptr,
     .palette = nullptr,

+ 1 - 0
components/gxepd2/panel_420_gdey042t81.h

@@ -62,6 +62,7 @@ inline const EpdPanelDescriptor PANEL_420_GDEY042T81 = {
     .window_sequence_len = {sizeof(PANEL_420_GDEY042T81_WINDOW_SEQ0) / sizeof(PANEL_420_GDEY042T81_WINDOW_SEQ0[0]), 0},
     .window_sequence_len = {sizeof(PANEL_420_GDEY042T81_WINDOW_SEQ0) / sizeof(PANEL_420_GDEY042T81_WINDOW_SEQ0[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x24, 0x00},
     .plane_write_cmd = {0x24, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x26, 0x00},  // writeImageForFullRefresh also re-writes this "previous RAM" bank before the 0x24 write above - not read by this full-refresh-only engine yet, captured for future partial-refresh use
     .plane_invert = {false, false},  // probe-derived
     .plane_invert = {false, false},  // probe-derived
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .palette = nullptr,
     .palette = nullptr,

+ 1 - 0
components/gxepd2/panel_420_gye042a87.h

@@ -62,6 +62,7 @@ inline const EpdPanelDescriptor PANEL_420_GYE042A87 = {
     .window_sequence_len = {sizeof(PANEL_420_GYE042A87_WINDOW_SEQ0) / sizeof(PANEL_420_GYE042A87_WINDOW_SEQ0[0]), 0},
     .window_sequence_len = {sizeof(PANEL_420_GYE042A87_WINDOW_SEQ0) / sizeof(PANEL_420_GYE042A87_WINDOW_SEQ0[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x24, 0x00},
     .plane_write_cmd = {0x24, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x26, 0x00},  // writeImageForFullRefresh also re-writes this "previous RAM" bank before the 0x24 write above - not read by this full-refresh-only engine yet, captured for future partial-refresh use
     .plane_invert = {false, false},  // probe-derived
     .plane_invert = {false, false},  // probe-derived
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .bits_per_pixel = 1,  // probe: white fill => all-bits wire byte 0xFF
     .palette = nullptr,
     .palette = nullptr,

+ 4 - 1
components/gxepd2/panel_426_gdeq0426t82.h

@@ -10,7 +10,9 @@
 // command (0x24) is ever written here - 0x26 is dropped entirely, same as
 // command (0x24) is ever written here - 0x26 is dropped entirely, same as
 // GxEPD2's own _initial_write priming pass (a dummy full-buffer clear
 // GxEPD2's own _initial_write priming pass (a dummy full-buffer clear
 // before the real content write) is dropped, since this engine always
 // before the real content write) is dropped, since this engine always
-// writes real content directly and re-initializes fully every cycle.
+// writes real content directly and re-initializes fully every cycle. The
+// 0x26 write is still recorded below, in plane_write_cmd_for_full_refresh,
+// so a future partial-refresh engine has it without a second capture pass.
 //
 //
 // Also the first panel whose RAM-window-set (_setPartialRamArea) layout
 // Also the first panel whose RAM-window-set (_setPartialRamArea) layout
 // differs from GxEPD2_213_Z98c's: SSD1677 addresses 0x44/0x45 in raw
 // differs from GxEPD2_213_Z98c's: SSD1677 addresses 0x44/0x45 in raw
@@ -81,6 +83,7 @@ inline const EpdPanelDescriptor PANEL_426_GDEQ0426T82 = {
     .window_sequence_len = {sizeof(PANEL_426_GDEQ0426T82_WINDOW) / sizeof(PANEL_426_GDEQ0426T82_WINDOW[0]), 0},
     .window_sequence_len = {sizeof(PANEL_426_GDEQ0426T82_WINDOW) / sizeof(PANEL_426_GDEQ0426T82_WINDOW[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x24, 0x00},
     .plane_write_cmd = {0x24, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x26, 0x00},  // see file header
     .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]),

+ 1 - 0
components/gxepd2/panel_583_gdeq0583t31.h

@@ -99,6 +99,7 @@ inline const EpdPanelDescriptor PANEL_583_GDEQ0583T31 = {
     .window_sequence_len = {sizeof(PANEL_583_GDEQ0583T31_WINDOW) / sizeof(PANEL_583_GDEQ0583T31_WINDOW[0]), 0},
     .window_sequence_len = {sizeof(PANEL_583_GDEQ0583T31_WINDOW) / sizeof(PANEL_583_GDEQ0583T31_WINDOW[0]), 0},
     .plane_count = 1,
     .plane_count = 1,
     .plane_write_cmd = {0x13, 0x00},
     .plane_write_cmd = {0x13, 0x00},
+    .plane_write_cmd_for_full_refresh = {0x10, 0x00},  // writeImageForFullRefresh also re-writes this "previous RAM" bank before the 0x13 write above - not read by this full-refresh-only engine yet, captured for future partial-refresh use
     .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]),

+ 13 - 2
tools/capture.py

@@ -267,6 +267,14 @@ def run_program(model: str, catalog: dict, mode: str, cxx: str) -> str:
             f'#include "{hdr_rel}"\n'
             f'#include "{hdr_rel}"\n'
             f'int main() {{ emit::print_power_sequences<{real_class}>("{macro_of(model)}"); return 0; }}\n'
             f'int main() {{ emit::print_power_sequences<{real_class}>("{macro_of(model)}"); return 0; }}\n'
         )
         )
+    elif mode == "full-refresh":
+        # emit_descriptor.h's print_full_refresh_extra_write - same splice
+        # workflow as --power, for plane_write_cmd_for_full_refresh.
+        main_src = (
+            f'#include "emit_descriptor.h"\n'
+            f'#include "{hdr_rel}"\n'
+            f'int main() {{ emit::print_full_refresh_extra_write<{real_class}>(); return 0; }}\n'
+        )
     else:
     else:
         header, fn = _PROGRAMS[mode]
         header, fn = _PROGRAMS[mode]
         main_src = (
         main_src = (
@@ -309,6 +317,9 @@ def main() -> None:
     ap.add_argument("--power", action="store_true",
     ap.add_argument("--power", action="store_true",
                      help="print just power_off_sequence/hibernate_sequence (for splicing into an existing "
                      help="print just power_off_sequence/hibernate_sequence (for splicing into an existing "
                           "panel_*.h), instead of emitting a full descriptor")
                           "panel_*.h), instead of emitting a full descriptor")
+    ap.add_argument("--full-refresh", action="store_true", dest="full_refresh",
+                     help="print just plane_write_cmd_for_full_refresh (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()
 
 
@@ -318,8 +329,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 or args.power:
-        mode = "sweep" if args.sweep else "diag" if args.diag else "power"
+    if args.sweep or args.diag or args.power or args.full_refresh:
+        mode = "sweep" if args.sweep else "diag" if args.diag else "power" if args.power else "full-refresh"
         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)

+ 136 - 0
tools/gxepd2_harness/emit_descriptor.h

@@ -335,6 +335,118 @@ template<typename Driver> PackedResolverFn make_resolver_bw_packed(uint8_t p0cmd
   };
   };
 }
 }
 
 
+// Plane-sized (>= PLANE_BURST_MIN) burst commands one call makes, from a
+// fresh instance with an untraced writeScreenBuffer() first (same trick
+// probe_plane_invert_values uses) to absorb the lazy _InitDisplay()/
+// _initial_write clear. Shared by probe_full_refresh_extra_write_cmd below so
+// it can tell a genuinely NEW burst from one the plain single-bitmap
+// writeImage() already makes on its own - a 2-plane (3-colour) driver's
+// single-bitmap writeImage(bitmap,...) forwards to writeImage(black, NULL,
+// ...), which bursts to BOTH its real plane_write_cmd[0] (real data) AND
+// plane_write_cmd[1] (a NULL-color blank-fill burst, same size) every time,
+// with no full-refresh involved at all - diffing against just plane_write_cmd
+// [0] would misread that second plane's ordinary burst as a full-refresh-only
+// extra write.
+template<typename Driver, typename Call> std::set<uint8_t> plane_burst_cmds(Call 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);
+  d.writeScreenBuffer();  // untraced: absorb lazy init + the _initial_write clear
+
+  const uint32_t plane_bytes = (uint32_t) Driver::WIDTH * Driver::HEIGHT / 8;
+  auto *buf = new uint8_t[plane_bytes];
+  memset(buf, 0xA5, plane_bytes);
+  Trace::start_event_record();
+  call(d, buf);
+  Trace::stop_event_record();
+  delete[] buf;
+
+  std::set<uint8_t> cmds;
+  for (const auto &c : group(0, Trace::events.size()))
+    if (c.data.size() >= PLANE_BURST_MIN)
+      cmds.insert(c.cmd);
+  return cmds;
+}
+
+// Detects a controller whose writeImageForFullRefresh() diverges from plain
+// writeImage() - e.g. SSD1680/SSD1619-family controllers write the same
+// bitmap to a "previous RAM" bank (a different command) before the normal
+// "current RAM" write plane_write_cmd[plane] already captures. GxEPD2_EPD's
+// base class defaults writeImageForFullRefresh() to call writeImage()
+// unchanged, so this probe generalizes across every controller without
+// hardcoding the known-affected families: it diffs the two calls' burst-
+// command sets and returns whatever writeImageForFullRefresh() adds. Returns
+// 0x00 (no divergence - by far the common case) or the extra command byte.
+template<typename Driver> uint8_t probe_full_refresh_extra_write_cmd() {
+  auto write_cmds = plane_burst_cmds<Driver>(
+      [](Peek<Driver> &d, uint8_t *buf) { d.writeImage(buf, 0, 0, Driver::WIDTH, Driver::HEIGHT); });
+  auto full_cmds = plane_burst_cmds<Driver>(
+      [](Peek<Driver> &d, uint8_t *buf) { d.writeImageForFullRefresh(buf, 0, 0, Driver::WIDTH, Driver::HEIGHT); });
+  for (uint8_t c : full_cmds)
+    if (!write_cmds.count(c))
+      return c;
+  return 0x00;
+}
+
+// Prints just the plane_write_cmd_for_full_refresh{} line (for splicing into
+// an existing hand-maintained panel_*.h, same workflow as
+// print_power_sequences / tools/capture.py --power). Self-contained like
+// print_power_sequences: derives p0cmd itself (the first plane-sized burst a
+// plain writeImage() produces) rather than trusting a shipped panel_*.h's
+// plane_write_cmd, which may have been hand-adjusted since it was captured.
+template<typename Driver> void print_full_refresh_extra_write() {
+  constexpr int16_t CS = 10, DC = 11, RST = 12, BUSY = 13;
+  Trace::set_dc_pin(DC);
+  Peek<Driver> probe(CS, DC, RST, BUSY);
+  Trace::last_busy_release_value = probe.busy_level() == HIGH ? LOW : HIGH;
+  probe.selectSPI(SPI, SPISettings());
+  probe.init(0);
+  probe.writeScreenBuffer();  // untraced: absorb lazy init + the _initial_write clear
+
+  const uint32_t plane_bytes = (uint32_t) Driver::WIDTH * Driver::HEIGHT / 8;
+  auto *buf = new uint8_t[plane_bytes];
+  memset(buf, 0xFF, plane_bytes);  // 0xFF/0x00, not a filler byte: doubles as the 1bpp-vs-packed check below
+  Trace::start_event_record();
+  probe.writeImage(buf, 0, 0, Driver::WIDTH, Driver::HEIGHT);
+  Trace::stop_event_record();
+  delete[] buf;
+
+  uint8_t p0cmd = 0x00;
+  int w0_white = -1;
+  for (const auto &c : group(0, Trace::events.size()))
+    if (c.data.size() >= PLANE_BURST_MIN) {
+      p0cmd = c.cmd;
+      w0_white = c.data[0];
+      break;
+    }
+
+  // A packed (>1bpp) driver's writeImageForFullRefresh expects its own packed
+  // bit depth, not the 1bpp-sized buffer above - probing it the same way a
+  // 1bpp panel is probed can over/under-read and crash (seen on GDEP073E01,
+  // 4bpp 7-colour). A 1bpp panel's white-fill wire byte is always 0x00 or
+  // 0xFF (see probe_plane_invert_values); anything else means packed, so bail
+  // instead of guessing - this field isn't meaningful there anyway (see
+  // emit_descriptor_impl's pv.is_1bpp gate).
+  if (w0_white != 0x00 && w0_white != 0xFF) {
+    printf("    // SKIPPED: packed (>1bpp) panel - writeImageForFullRefresh's dual-write behaviour is "
+           "specific to the 1bpp plane model, not applicable here\n");
+    return;
+  }
+
+  const uint8_t extra = probe_full_refresh_extra_write_cmd<Driver>();
+  if (extra)
+    printf("    .plane_write_cmd_for_full_refresh = {0x%02X, 0x00},  // writeImageForFullRefresh also "
+           "re-writes this \"previous RAM\" bank before the 0x%02X write above - captured for future "
+           "partial-refresh use, not read by this full-refresh-only engine yet\n",
+           extra, p0cmd);
+  else
+    printf("    // writeImageForFullRefresh == writeImage for this controller: no "
+           ".plane_write_cmd_for_full_refresh entry needed (0x00 default already correct)\n");
+}
+
 // Captures powerOff() or hibernate() as its own standalone command sequence -
 // Captures powerOff() or hibernate() as its own standalone command sequence -
 // NOT folded into the refresh_seq phase-segmentation above, and NOT captured
 // 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()
 // from the same live session as each other. Every GxEPD2 driver's powerOff()
@@ -705,6 +817,18 @@ void emit_descriptor_impl(const char *class_name, const char *macro, const Packe
   std::vector<Cmd> &power_off_seq = power_captured.first;
   std::vector<Cmd> &power_off_seq = power_captured.first;
   std::vector<Cmd> &hibernate_seq = power_captured.second;
   std::vector<Cmd> &hibernate_seq = power_captured.second;
 
 
+  // plane_write_cmd_for_full_refresh - computed here, BEFORE the "// GENERATED"
+  // marker below, not alongside the printf()s that use it: every probe in this
+  // function runs its own fresh Peek<Driver> through an un-traced init()/
+  // writeScreenBuffer() first (see plane_burst_cmds), which prints raw
+  // pinMode()/digitalWrite()/etc lines straight to stdout outside the
+  // Trace::event_recording bracket - harmless for every OTHER probe above
+  // because capture()/run_program() in tools/capture.py slice captured stdout
+  // from the FIRST "// GENERATED" marker onward, dropping that noise. Calling
+  // this probe any later - after the marker's printed - would leak its raw
+  // trace lines into the middle of the emitted descriptor instead.
+  const uint8_t full_refresh_extra = pv.is_1bpp ? probe_full_refresh_extra_write_cmd<Driver>() : 0x00;
+
   // --- 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");
@@ -781,6 +905,18 @@ void emit_descriptor_impl(const char *class_name, const char *macro, const Packe
   }
   }
   printf("    .plane_count = %zu,\n", nplanes);
   printf("    .plane_count = %zu,\n", nplanes);
   printf("    .plane_write_cmd = {0x%02X, 0x%02X},\n", p0cmd, p1cmd);
   printf("    .plane_write_cmd = {0x%02X, 0x%02X},\n", p0cmd, p1cmd);
+  // full_refresh_extra was computed above, before the "// GENERATED" marker -
+  // see that computation's comment for why. A packed (>1bpp) driver's
+  // writeImageForFullRefresh takes the same single-bitmap buffer shape, but
+  // its _writeImage expects a buffer already packed to its own bit depth -
+  // probing it with a 1bpp-sized buffer over/under-reads and crashes some
+  // drivers (e.g. GDEP073E01, 4bpp 7-colour), hence the pv.is_1bpp gate above;
+  // packed panels just keep the {0,0} default.
+  if (full_refresh_extra)
+    printf("    .plane_write_cmd_for_full_refresh = {0x%02X, 0x00},  // writeImageForFullRefresh also "
+           "re-writes this \"previous RAM\" bank before the 0x%02X write above - not read by this "
+           "full-refresh-only engine yet, captured for future partial-refresh use\n",
+           full_refresh_extra, p0cmd);
   // Fields below are emitted in struct-declaration order (plane_invert,
   // Fields below are emitted in struct-declaration order (plane_invert,
   // plane_width, line_order, bits_per_pixel, palette, palette_len,
   // plane_width, line_order, bits_per_pixel, palette, palette_len,
   // blank_code) so the designated initializer stays well-formed.
   // blank_code) so the designated initializer stays well-formed.