From 1ca2990aebf0aa19ccc2ace8d02ca6d4f501ad17 Mon Sep 17 00:00:00 2001 From: David Lee <247393336+davelee98@users.noreply.github.com> Date: Sun, 9 Aug 2026 09:18:32 -0400 Subject: [PATCH 1/5] build(deps): pin bb_epaper to upstream 5dccfbb; drop the esp32-s3-E1004 env bb_epaper has no releases, so the unpinned `[env]` URL meant CI and local builds each got whatever HEAD was at install time. Locally that was c651b2a while the source already carried a workaround for 71f6e70 (see epdAlignCustomPartialRamMode) -- a commit that was not in the installed library. Pinning closes that divergence rather than creating one. 5dccfbb is "Added support for the Seeed reTerminal E1004 and its 13.3 Spectra6 1200x1600 panel", which replaces the limengdu fork pin the E1004 env carried for PR bitbank2#32. The esp32-s3-E1004 env goes with it: it was the same hardware as esp32-s3-N32R8-extuart, and the panel is selected at runtime by panel_ic_type, not at build time. CI matrix drops 12 -> 11 targets. NOTE: E1004 hardware built from esp32-s3-N32R8-extuart now runs a 9-bit (512 B) zlib window instead of 15-bit. That is a hard reject at the zlib header for any stream declaring a larger window (od_zlib_stream.c:641), not a slower decode -- deliberate, but worth knowing if a client ever compressed with wbits=15 for this target. --- .github/firmware-targets.json | 9 --------- platformio.ini | 23 ++++++++++------------- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/.github/firmware-targets.json b/.github/firmware-targets.json index 618e1c5..1db52be 100644 --- a/.github/firmware-targets.json +++ b/.github/firmware-targets.json @@ -66,15 +66,6 @@ "flash_freq": "80m", "bootloader_offset": "0x0000" }, - { - "env": "esp32-s3-E1004", - "family": "esp32", - "chip": "esp32-s3", - "flash_size": "32MB", - "flash_mode": "dio", - "flash_freq": "80m", - "bootloader_offset": "0x0000" - }, { "env": "esp32-c6-N4", "family": "esp32", diff --git a/platformio.ini b/platformio.ini index 3472b08..9e8d6f9 100644 --- a/platformio.ini +++ b/platformio.ini @@ -44,7 +44,6 @@ default_envs = esp32-s3-N32R8 esp32-s3-N32R8-extuart esp32-s3-N16R8-extuart - esp32-s3-E1004 esp32-c3-N4 esp32-c3-N16 esp32-c6-N4 @@ -52,8 +51,12 @@ default_envs = [env] lib_deps = - https://github.com/bitbank2/bb_epaper.git - h2zero/NimBLE-Arduino@^2.5.0 + ; Pinned: bb_epaper has no releases, so an unpinned URL means CI and local + ; builds get whatever HEAD was at install time. 5dccfbb is "Added support for + ; the Seeed reTerminal E1004 and its 13.3 Spectra6 1200x1600 panel" -- the + ; commit that landed E1004 upstream, replacing the limengdu fork pin. + https://github.com/bitbank2/bb_epaper.git#5dccfbb + h2zero/NimBLE-Arduino@^2.5.0 extra_scripts = pre:scripts/factory_config_gen.py build_src_filter = @@ -303,16 +306,10 @@ board_upload.maximum_ram_size = 327680 board_upload.flash_size = 32MB monitor_speed = 115200 -; Seeed reTerminal E1004 — same hardware as N32R8-extuart; pinned bb_epaper for T133A01. -[env:esp32-s3-E1004] -extends = env:esp32-s3-N32R8-extuart -; Pin bb_epaper to limengdu PR bitbank2#32 (T133A01) until merged upstream. -lib_deps = - https://github.com/limengdu/bb_epaper.git#95fd94afe39cd7db32bef7c70eea06d654264ff6 - h2zero/NimBLE-Arduino@^2.5.0 -build_flags = - ${env:esp32-s3-N32R8-extuart.build_flags} - -DOPENDISPLAY_ZLIB_WINDOW_BITS=15 +; The Seeed reTerminal E1004 has no env of its own: it is the same hardware as +; esp32-s3-N32R8-extuart, and its 13.3" Spectra6 panel is selected at runtime by +; panel_ic_type in the device config, not at build time. Flash it with +; esp32-s3-N32R8-extuart. ; ESP targets pin pioarduino 55.03.39 (Arduino 3.3.9 / IDF 5.5.4), by exact version ; so it cannot drift CI vs local. From 0363a93bcdbf322b6acff90c1ecb1ba7f2fa78f7 Mon Sep 17 00:00:00 2001 From: David Lee <247393336+davelee98@users.noreply.github.com> Date: Sun, 9 Aug 2026 09:25:08 -0400 Subject: [PATCH 2/5] feat(display): bufferless dual-controller panel module, runtime-gated Replaces the E1004 shim in display_service.cpp with src/split_panel.{h,cpp}. The shim was guarded by `#ifdef BBEP_T133A01` -- a *bb_epaper* define from the limengdu fork, not a firmware one. Repinning to upstream removed the define, so all 12 guarded regions compiled to nothing while `pio run -e esp32-s3-E1004` still reported SUCCESS: firmware that could not drive the panel, with no error and no log line. The new module is gated at runtime on `bbep.iFlags & BBEP_SPLIT_BUFFER`, so panel support cannot disappear with a library change, and static_asserts turn a future rename into a compile error on every target. Still bufferless: upstream's own dual-CS writer (bbepWriteImage4bppDual, via bbepWritePlane) needs a 960 KB ucScreen, so the data phase is driven from the library's own primitives instead -- cs_mode = CMD_CS_NONE, manual chip-select, one DTM per controller, chunked bbepWriteData. Same idiom the library uses internally; only the byte source differs. The other three phases need no help: the init sequence carries CMD_CS1/CMD_CS1_CS2 opcodes, and bbepRefresh/ bbepSleep broadcast DRF/POFF off the SPLIT_BUFFER flag. Keying on the flag rather than a panel id also covers EP81_SPECTRA_1024x576 (wire 0x2B), which previously took the generic path and drove only its left controller. Fixed rather than carried over from the shim: - run length computed in uint16_t, so a >=65536-byte chunk truncated to 0 and spun forever in a loop with no watchdog feed - truncated/overrun frames dropped silently and still refreshed as success; splitPanelCloseFrame now returns a verdict and both refresh sites honour it - no bound in the raw write path, and no busy gate before streaming - waitforrefresh's E1004 shortcut assumed bbepRefresh waited. True of the fork, false upstream -- it could report success before BUSY even asserted and let the rail be cut mid-refresh - no POFF after refresh: the C bbepRefresh issues DRF and returns, and only the unused C++ BBEPAPER::refresh wrapper powers down Also drops three branches that are now dead: the `|| splitPanelUsed()` in both partial-update NACKs (bwgbry_split reports 4 bpp, so the bpp test already excludes them) and the forced REFRESH_FULL (the Spectra6 table rows carry a NULL pInitFast, so bbepRefresh falls back to pInitFull and both modes emit an identical sequence). boot_screen keeps its two half-passes -- it already emits in the sink's wire order, so it just feeds it, and painting half a frame at a time still avoids rasterising 1200x1600 twice. All 11 CI targets build (incl. esp32-wrover-e-N4R8, absent from default_envs). Cost ~1.6 KB flash per target, no RAM. UNVERIFIED ON HARDWARE. A clean build proves linkage, not function -- that is exactly what the old gate got wrong. Open bench questions: whether the init sequence bbepRefresh re-sends leaves streamed RAM intact (it has no EPD_RESET, so it should), whether holding CS across the boot screen's rasterisation is tolerated, and the 8.1" behaviour change above. --- src/boot_screen.cpp | 59 ++++----- src/display_service.cpp | 274 +++++++++------------------------------- src/display_service.h | 6 - src/split_panel.cpp | 224 ++++++++++++++++++++++++++++++++ src/split_panel.h | 92 ++++++++++++++ 5 files changed, 409 insertions(+), 246 deletions(-) create mode 100644 src/split_panel.cpp create mode 100644 src/split_panel.h diff --git a/src/boot_screen.cpp b/src/boot_screen.cpp index 20f974e..a78c770 100644 --- a/src/boot_screen.cpp +++ b/src/boot_screen.cpp @@ -8,6 +8,7 @@ #include #include "qr/qrcode.h" #include "display_service.h" +#include "split_panel.h" #include "od_log.h" #if __has_include("logo_bitmap.h") #include "logo_bitmap.h" @@ -905,10 +906,14 @@ bool writeBootScreenWithQr() { int textStartY = textY; const uint16_t footerInfoY = (uint16_t)(footerY0 + (footerPadTop + footerInfoH - 7 * footerInfoScale) / 2); - // Dual-controller E1004 (bwgbry_split): left half-plane then right (continuous DTM). - const bool e1004Stream = e1004_panel_used(); - const int e1004HalfPasses = e1004Stream ? 2 : 1; - const uint16_t e1004HalfPitch = (uint16_t)(pitch / 2); + // Dual-controller panels (bwgbry_split): paint and emit the left half-plane, + // then the right. That is exactly the wire order splitPanelSinkBytes() expects, + // so the halves land in the right columns of the framebuffer with no extra + // bookkeeping here -- and painting one half at a time still avoids rasterising + // the full frame twice. + const bool splitStream = splitPanelUsed(); + const int splitHalfPasses = splitStream ? 2 : 1; + const uint16_t splitHalfPitch = (uint16_t)(pitch / 2); uint8_t* row = staticRowBuffer; // bb_epaper 4-gray (scheme 5) needs the packed 2bpp image split into two @@ -925,18 +930,12 @@ bool writeBootScreenWithQr() { return false; } const int planePasses = (gray4Split || colorSwatchPlane1) ? 2 : 1; - for (int halfPass = 0; halfPass < e1004HalfPasses; halfPass++) { - if (e1004Stream) { - if (halfPass == 0) { - if (!e1004_begin_plane()) { - od_log_error("Boot screen: E1004 dual-CS plane open failed"); - return false; - } - } else if (!e1004_advance_to_cs2()) { - od_log_error("Boot screen: E1004 CS2 advance failed"); - e1004_end_plane(); - return false; - } + for (int halfPass = 0; halfPass < splitHalfPasses; halfPass++) { + // Only pass 0 opens the frame; the sink crosses to the right half on its + // own once the left half-plane's worth of bytes has arrived. + if (splitStream && halfPass == 0 && !splitPanelBeginFrame()) { + od_log_error("Boot screen: split panel frame open failed"); + return false; } for (int pass = 0; pass < planePasses; pass++) { const int bitSel = pass; // pass 0 -> LSB/PLANE_0, pass 1 -> MSB/PLANE_1 @@ -944,12 +943,12 @@ bool writeBootScreenWithQr() { : (colorSwatchPlane1 ? (pass == 0 ? PLANE_0 : PLANE_1) : (useBitplanes ? PLANE_0 : getplane())); #if defined(TARGET_ESP32) && defined(OPENDISPLAY_FASTEPD) - if (!fastepd_driver_used() && !e1004Stream) { + if (!fastepd_driver_used() && !splitStream) { bbepSetAddrWindow(&bbep, 0, 0, w, h); bbepStartWrite(&bbep, targetPlane); } #else - if (!e1004Stream) { + if (!splitStream) { bbepSetAddrWindow(&bbep, 0, 0, w, h); bbepStartWrite(&bbep, targetPlane); } @@ -963,13 +962,13 @@ bool writeBootScreenWithQr() { : (uint16_t)(fwY + ls); const uint16_t k2Y = (uint16_t)(k1Y + ls); const bool colorPlanePass = colorSwatchPlane1 && pass == 1; - // E1004 half-pass: only paint the half we will stream (avoids 2× full-frame work). - const uint16_t xPaint0 = e1004Stream ? (uint16_t)(halfPass * (w / 2u)) : (uint16_t)0; - const uint16_t xPaint1 = e1004Stream ? (uint16_t)(xPaint0 + (w / 2u)) : w; + // Split half-pass: only paint the half we will emit (avoids 2× full-frame work). + const uint16_t xPaint0 = splitStream ? (uint16_t)(halfPass * (w / 2u)) : (uint16_t)0; + const uint16_t xPaint1 = splitStream ? (uint16_t)(xPaint0 + (w / 2u)) : w; for (uint16_t y_native = 0; y_native < h; y_native++) { - if (e1004Stream) { - memset(row + (size_t)halfPass * e1004HalfPitch, - colorPlanePass ? 0x00 : whiteValue, e1004HalfPitch); + if (splitStream) { + memset(row + (size_t)halfPass * splitHalfPitch, + colorPlanePass ? 0x00 : whiteValue, splitHalfPitch); } else { memset(row, colorPlanePass ? 0x00 : whiteValue, pitch); } @@ -1030,8 +1029,8 @@ bool writeBootScreenWithQr() { fastepd_boot_write_row(y_native, row, pitch); } else if (gray4Split) { writeGray4PlaneRow(row, pitch, planePitch, w, bitSel); - } else if (e1004Stream) { - e1004_write_stream_bytes(row + (size_t)halfPass * e1004HalfPitch, e1004HalfPitch); + } else if (splitStream) { + splitPanelSinkBytes(row + (size_t)halfPass * splitHalfPitch, splitHalfPitch); } else { odWatchdogBreadcrumb(OD_WDT_PHASE_STREAM); bbepWriteData(&bbep, row, pitch); @@ -1039,8 +1038,8 @@ bool writeBootScreenWithQr() { #else if (gray4Split) { writeGray4PlaneRow(row, pitch, planePitch, w, bitSel); - } else if (e1004Stream) { - e1004_write_stream_bytes(row + (size_t)halfPass * e1004HalfPitch, e1004HalfPitch); + } else if (splitStream) { + splitPanelSinkBytes(row + (size_t)halfPass * splitHalfPitch, splitHalfPitch); } else { odWatchdogBreadcrumb(OD_WDT_PHASE_STREAM); bbepWriteData(&bbep, row, pitch); @@ -1050,7 +1049,9 @@ bool writeBootScreenWithQr() { } } // halfPass - if (e1004Stream) e1004_end_plane(); + // No close step here: the chip selects stay held until splitPanelCloseFrame() + // releases them just before the refresh, in refreshBootScreenFull(). Closing + // early would make that call report an already-closed frame and skip the refresh. #if defined(TARGET_ESP32) && defined(OPENDISPLAY_FASTEPD) if (fastepd_driver_used()) { diff --git a/src/display_service.cpp b/src/display_service.cpp index 3d9681f..0ed04bd 100644 --- a/src/display_service.cpp +++ b/src/display_service.cpp @@ -12,6 +12,7 @@ #include "communication.h" #include "encryption.h" #include "boot_screen.h" +#include "split_panel.h" #include "link_owner.h" #include "session_guard.h" #include "touch_input.h" @@ -159,13 +160,6 @@ void bbepWriteCmd(BBEPDISP *pBBEP, uint8_t cmd); void bbepCMD2(BBEPDISP *pBBEP, uint8_t cmd1, uint8_t cmd2); void bbepWaitBusy(BBEPDISP *pBBEP); bool bbepIsBusy(BBEPDISP *pBBEP); -#ifdef BBEP_T133A01 -void bbepSetCS2(BBEPDISP *pBBEP, uint8_t cs); -void bbepWriteCmdData(BBEPDISP *pBBEP, uint8_t cmd, const uint8_t *pData, int iLen); -void bbepStartDataStream(BBEPDISP *pBBEP, uint8_t cmd); -void bbepWriteDataStreamByte(BBEPDISP *pBBEP, uint8_t data); -void bbepEndDataStream(BBEPDISP *pBBEP); -#endif void flashLed(uint8_t color, uint8_t brightness); bool waitforrefresh(int timeout); @@ -202,128 +196,6 @@ static void prepareEpdRailForBoot() { #endif } -#ifdef BBEP_T133A01 -// CS2 must be set before bbepInitIO() so dual-chip init reaches both controllers. -static void e1004InitPanel(void) { - const DisplayConfig& d = globalConfig.displays[0]; - bbepSetCS2(&bbep, e1004_cs2_pin()); - odWatchdogBreadcrumb(OD_WDT_PHASE_INIT_SEQ); - // WDT-DEBUG: EPD session stage instrumentation, added alongside the hardware - // watchdog work -- safe to delete this block if it's no longer needed. - od_log_debug("[EPD session][WDT] e1004InitPanel: INIT_SEQ (dual-controller)"); - odWatchdogFeed(); // bbepInitIO sends pInitFull internally (~240 s worst case) - bbepInitIO(&bbep, d.dc_pin, d.reset_pin, d.busy_pin, d.cs_pin, d.data_pin, d.clk_pin, 8000000); -} - -// Half-panel DTM for bwgbry_split: CS held for left half, then right (no FB). -static bool e1004GeometryOk = false; -static bool e1004StreamOpen = false; -static bool e1004OnLeftHalf = true; -static uint32_t e1004HalfBytesWritten = 0; -static uint32_t e1004HalfPlaneBytes = 0; - -void e1004_end_plane(void); - -static uint8_t e1004_panel_byte(uint8_t packed) { - auto nibble = [](uint8_t c) -> uint8_t { - c &= 0x0f; - switch (c) { - case 0x00: case 0x01: case 0x02: case 0x03: case 0x05: case 0x06: - return c; - default: - return 0x00; - } - }; - return (uint8_t)((nibble(packed >> 4) << 4) | nibble(packed)); -} - -static void e1004_ccset_both(void) { - uint8_t data = 0x01; - digitalWrite(bbep.iCS2Pin, LOW); - bbep.iCSPin = bbep.iCS1Pin; - bbepWriteCmdData(&bbep, 0xe0, &data, 1); - digitalWrite(bbep.iCS2Pin, HIGH); - bbep.iCSPin = bbep.iCS1Pin; - bbepWaitBusy(&bbep); - delay(10); -} - -static uint32_t e1004_half_plane_bytes(void) { - return ((uint32_t)bbep.native_width / 4u) * (uint32_t)bbep.native_height; -} - -bool e1004_begin_plane(void) { - if (!e1004_panel_used() || !e1004GeometryOk) return false; - if (e1004StreamOpen) e1004_end_plane(); - e1004_ccset_both(); - e1004HalfPlaneBytes = e1004_half_plane_bytes(); - e1004HalfBytesWritten = 0; - e1004OnLeftHalf = true; - bbep.iCSPin = bbep.iCS1Pin; - bbepStartDataStream(&bbep, UC8151_DTM1); - e1004StreamOpen = true; - return true; -} - -bool e1004_advance_to_cs2(void) { - if (!e1004StreamOpen || !e1004OnLeftHalf) return false; - bbepEndDataStream(&bbep); - e1004OnLeftHalf = false; - e1004HalfBytesWritten = 0; - bbep.iCSPin = bbep.iCS2Pin; - bbepStartDataStream(&bbep, UC8151_DTM1); - return true; -} - -void e1004_end_plane(void) { - if (!e1004StreamOpen) return; - bbepEndDataStream(&bbep); - bbep.iCSPin = bbep.iCS1Pin; - e1004StreamOpen = false; - e1004OnLeftHalf = true; - e1004HalfBytesWritten = 0; -} - -void e1004_write_stream_bytes(const uint8_t* data, uint16_t len) { - if (!e1004StreamOpen || !data || len == 0) return; - uint8_t scratch[128]; - uint16_t off = 0; - while (off < len) { - uint16_t n = (uint16_t)(len - off); - if (n > sizeof(scratch)) n = sizeof(scratch); - for (uint16_t i = 0; i < n; i++) scratch[i] = e1004_panel_byte(data[off + i]); - SPI.writeBytes(scratch, n); - off = (uint16_t)(off + n); - } - e1004HalfBytesWritten += len; -} - -static void e1004_sink_bytes(uint8_t* data, uint32_t len) { - while (len > 0 && e1004StreamOpen) { - if (e1004HalfPlaneBytes == 0) return; - uint32_t space = e1004HalfPlaneBytes - e1004HalfBytesWritten; - if (space == 0) { - if (e1004OnLeftHalf) { - if (!e1004_advance_to_cs2()) return; - continue; - } - return; - } - uint16_t take = (len < space) ? (uint16_t)len : (uint16_t)((space > 0xFFFFu) ? 0xFFFFu : space); - e1004_write_stream_bytes(data, take); - data += take; - len -= take; - if (e1004OnLeftHalf && e1004HalfBytesWritten >= e1004HalfPlaneBytes) { - if (!e1004_advance_to_cs2()) return; - } - } -} -#else -bool e1004_begin_plane(void) { return false; } -bool e1004_advance_to_cs2(void) { return false; } -void e1004_end_plane(void) {} -void e1004_write_stream_bytes(const uint8_t* data, uint16_t len) { (void)data; (void)len; } -#endif // bb_epaper 71f6e70 replaced EP397/EP426 full-init RAM windows with SET_ORIENTATION // (flip180=0 → 0x11=0x02 on 800-wide) while part inits and our partial helpers still @@ -361,37 +233,29 @@ static void epdAlignCustomPartialRamMode(void) { } } -// Sets bbep.type/native dims/rotation and (E1004) e1004GeometryOk from globalConfig. +// Sets bbep.type/native dims/rotation from globalConfig, and re-attaches the +// split-panel framebuffer (the memset below clears bbep.ucScreen). static void configureBbepPanelGeometry(void) { memset(&bbep, 0, sizeof(BBEPDISP)); int panelType = mapEpd(globalConfig.displays[0].panel_ic_type); bbepSetPanelType(&bbep, panelType); int rotation = globalConfig.displays[0].rotation * 90; -#ifdef BBEP_T133A01 - e1004GeometryOk = false; - if (e1004_panel_used()) { - rotation = 0; // host bakes rotation into packed image - if (globalConfig.displays[0].pixel_width != bbep.native_width || - globalConfig.displays[0].pixel_height != bbep.native_height || - globalConfig.displays[0].color_scheme != OD_COLOR_SCHEME_BWGBRY_SPLIT) { - od_log_error("ERROR: E1004 requires a 1200x1600 bwgbry_split (8) display config"); - } else { - e1004GeometryOk = true; - } - } -#endif + // Dual-controller panels keep native orientation: the host bakes rotation into + // the packed image, and the framebuffer layout bbepWriteImage4bppDual() reads + // is native-order. + if (splitPanelUsed()) rotation = 0; bbepSetRotation(&bbep, rotation); + // After the memset above cleared bbep.ucScreen; re-attaches the framebuffer. + splitPanelConfigureGeometry(); } static void initBbepPanelSession() { const DisplayConfig& d = globalConfig.displays[0]; -#ifdef BBEP_T133A01 - if (e1004_panel_used()) { - e1004InitPanel(); + if (splitPanelUsed()) { + splitPanelInitIo(); delay(200); return; } -#endif odWatchdogBreadcrumb(OD_WDT_PHASE_INIT_SEQ); // WDT-DEBUG: EPD session stage instrumentation, added alongside the hardware // watchdog work -- safe to delete this block if it's no longer needed. @@ -536,13 +400,10 @@ static bool epdSessionAcquire(bool partialInit) { if (!epdSessionUsesFastepd()) { const DisplayConfig& d = globalConfig.displays[0]; configureBbepPanelGeometry(); -#ifdef BBEP_T133A01 - if (e1004_panel_used()) { - e1004InitPanel(); + if (splitPanelUsed()) { + splitPanelInitIo(); epdSessionInitWasPartial = false; - } else -#endif - { + } else { odWatchdogBreadcrumb(OD_WDT_PHASE_INIT_SEQ); // WDT-DEBUG: EPD session stage instrumentation, added alongside the // hardware watchdog work -- safe to delete this line if it's no longer needed. @@ -574,12 +435,11 @@ static bool epdSessionAcquire(bool partialInit) { // Phase 1: full re-init on warm re-acquire (HW reset => registers identical // to cold, safest). Phase 2a will skip bbepWakeUp + resend only on change. if (!epdSessionUsesFastepd()) { -#ifdef BBEP_T133A01 - if (e1004_panel_used()) { + if (splitPanelUsed()) { + // Nothing to re-send: the controllers keep their state while the + // rail is warm, and a re-init would cost a full bring-up. epdSessionInitWasPartial = false; - } else -#endif - { + } else { odWatchdogBreadcrumb(OD_WDT_PHASE_INIT_SEQ); // WDT-DEBUG: EPD session stage instrumentation, added alongside the // hardware watchdog work -- safe to delete this line if it's no longer needed. @@ -664,9 +524,15 @@ static bool refreshBootScreenFull() { od_log_debug("[EPD session][WDT] BOOT_REFRESH: entering bbepRefresh(REFRESH_FULL)"); od_log_info("EPD refresh: FULL (boot)"); touchSuspendForEpdRefresh(); + // Dual-controller panels hold both chip selects open across the frame; release + // them before DRF. A false return means the frame was short or faulted, so the + // panel must not be refreshed with it. + if (splitPanelUsed() && !splitPanelCloseFrame()) return false; odWatchdogFeed(); // reload before entering bb_epaper (may block ~240 s) bbepRefresh(&bbep, REFRESH_FULL); - return waitforrefresh(60); + const bool ok = waitforrefresh(60); + splitPanelPowerOff(); + return ok; } static void cleanup_partial_write_state(void); @@ -943,11 +809,7 @@ int mapEpd(int id){ case 0x003F: return EP31_240x320; case 0x0040: return EP75YR_800x480; case 0x0041: return EP_PANEL_UNDEFINED; -#ifdef BBEP_T133A01 - case OD_PANEL_IC_EP133A_SPECTRA_1200X1600: return EP133A_SPECTRA_1200x1600; // 0x0042, Seeed reTerminal E1004 -#else - case 0x0042: return EP_PANEL_UNDEFINED; -#endif + case OD_PANEL_IC_EP133A_SPECTRA_1200X1600: return EP133_SPECTRA_1200x1600; // 0x0042, Seeed reTerminal E1004 case 0x0043: return EP154_200x200_4GRAY; case 0x0044: return EP42B_400x300_4GRAY; case 0x0045: return EP397_800x480; @@ -980,31 +842,10 @@ bool fastepd_driver_used(void) { #endif } -bool e1004_panel_used(void) { -#ifdef BBEP_T133A01 - if (globalConfig.display_count < 1) return false; - return globalConfig.displays[0].panel_ic_type == OD_PANEL_IC_EP133A_SPECTRA_1200X1600; -#else - return false; -#endif -} - -// cs_pin_2; 0 or 0xFF defaults to GPIO2. -uint8_t e1004_cs2_pin(void) { - uint8_t p = globalConfig.displays[0].cs_pin_2; - if (p == 0 || p == 0xFF) return 2; - return p; -} - bool waitforrefresh(int timeout){ #if defined(TARGET_ESP32) && defined(OPENDISPLAY_FASTEPD) if (fastepd_driver_used()) return fastepd_wait_refresh(timeout); #endif - if (e1004_panel_used() && !bbepIsBusy(&bbep)) { - // bbepRefresh already waited; idle here means refresh finished. - od_log_info("Refresh completed inside bb_epaper"); - return true; - } odWatchdogBreadcrumb(OD_WDT_PHASE_REFRESH_WAIT); // Poll at 10 ms (was 100 ms) so a ~0.5 s refresh returns up to ~90 ms sooner. // BUSY asserts within µs of MASTER_ACTIVATE, so the i==0 "never went busy" @@ -2219,12 +2060,9 @@ static void directWriteSinkBytes(uint8_t* data, uint32_t len) { // Panel data path. Repeats are filtered inside odWatchdogBreadcrumb(), so a // per-call stamp here costs one comparison. odWatchdogBreadcrumb(OD_WDT_PHASE_STREAM); -#ifdef BBEP_T133A01 - if (e1004_panel_used()) { - if (e1004GeometryOk) e1004_sink_bytes(data, len); - } else -#endif - { + if (splitPanelUsed()) { + splitPanelSinkBytes(data, len); + } else { bbepWriteData(&bbep, data, (int)len); } directWriteBytesWritten += len; @@ -2245,6 +2083,13 @@ void cleanupDirectWriteState(bool refreshDisplay) { directWriteTotalBytes = 0; directWriteRefreshMode = 0; directWriteStartTime = 0; + // Release the dual-controller chip selects before anything else touches the + // panel. An aborted transfer (disconnect, timeout, decompress failure) reaches + // here with cs_mode still CMD_CS_NONE, and leaving it there means every + // subsequent bb_epaper write -- including the power-down below -- silently + // reaches no controller at all. Idempotent when no frame was open; the return + // value is the completeness verdict, which only the refresh path cares about. + if (splitPanelUsed()) (void)splitPanelCloseFrame(); // Panel power acts only while a transfer/refresh is actually in flight // (PWR_ACTIVE). refreshDisplay==true is a terminal teardown (disconnect, // 15-min timeout, mid-stream error) -> power fully off. refreshDisplay==false @@ -2254,9 +2099,6 @@ void cleanupDirectWriteState(bool refreshDisplay) { if (refreshDisplay) epdSessionForceOff(); else epdSessionRelease(true); } -#ifdef BBEP_T133A01 - if (e1004_panel_used()) e1004_end_plane(); -#endif if (directWriteTouchSuspended) { touchResumeAfterEpdRefresh(); directWriteTouchSuspended = false; @@ -2312,14 +2154,11 @@ static void directWriteActivatePanel(void) { fastepd_direct_write_reset(); } else #endif -#ifdef BBEP_T133A01 - if (e1004_panel_used()) { - if (!e1004_begin_plane()) { - od_log_error("ERROR: E1004 dual-CS plane open failed"); + if (splitPanelUsed()) { + if (!splitPanelBeginFrame()) { + od_log_error("ERROR: split panel frame open failed"); } - } else -#endif - { + } else { bbepSetAddrWindow(&bbep, 0, 0, globalConfig.displays[0].pixel_width, globalConfig.displays[0].pixel_height); bbepStartWrite(&bbep, directWriteBitplanes ? PLANE_0 : getplane()); } @@ -2426,10 +2265,14 @@ void handlePartialWriteStart(uint8_t* data, uint16_t len) { uint16_t dispW = globalConfig.displays[0].pixel_width; uint16_t dispH = globalConfig.displays[0].pixel_height; - if (getBitsPerPixel() != 1 || e1004_panel_used()) { + if (getBitsPerPixel() != 1) { // bb_epaper partial refresh support is effectively non-existent for // 2bpp+ panels, and physical panels may not support that mode either. // This protocol uses two 1bpp controller planes as old/new image memory. + // + // Dual-controller panels need no clause of their own: they are + // bwgbry_split, which getBitsPerPixel() reports as 4, so they are already + // excluded here on bpp alone. send_direct_write_nack(0x76, OD_ERR_PARTIAL_UNSUPPORTED, false); return; } @@ -2619,7 +2462,10 @@ static void directWriteFinishAndRefresh(uint8_t* data, uint16_t len, uint8_t end imageWriteLogFinish(directWriteBytesWritten, directWriteTotalBytes); int refreshMode = REFRESH_FULL; if (data != nullptr && len >= 1 && data[0] == 1) refreshMode = REFRESH_FAST; - if (e1004_panel_used()) refreshMode = REFRESH_FULL; // fast re-init would wipe RAM + // No dual-controller override here: the Spectra6 panel table entries carry a + // NULL pInitFast, and bbepRefresh() falls back to pInitFull when that is unset, + // so FAST and FULL emit an identical sequence for them. The forced FULL this + // replaces was guarding against fork behaviour that no longer exists. const char* modeName = (refreshMode == REFRESH_FAST) ? "FAST" : "FULL"; if (data != nullptr && len > 0) { od_log_info("EPD refresh: %s (mode=%d, end payload 0x%02X)", modeName, refreshMode, data[0]); @@ -2651,12 +2497,16 @@ static void directWriteFinishAndRefresh(uint8_t* data, uint16_t len, uint8_t end } else #endif { -#ifdef BBEP_T133A01 - if (e1004_panel_used()) e1004_end_plane(); -#endif - odWatchdogFeed(); // reload before entering bb_epaper (may block ~240 s) - bbepRefresh(&bbep, refreshMode); - refreshSuccess = waitforrefresh(60); + // Dual-controller panels hold both chip selects open across the frame; + // release them before DRF. A false return means the frame was short or + // faulted, so skip the refresh rather than commit it to the glass. + if (splitPanelUsed() && !splitPanelCloseFrame()) refreshMode = -1; + if (refreshMode >= 0) { + odWatchdogFeed(); // reload before entering bb_epaper (may block ~240 s) + bbepRefresh(&bbep, refreshMode); + refreshSuccess = waitforrefresh(60); + splitPanelPowerOff(); + } // No bbepSleep here: cleanupDirectWriteState(false) releases the session, // keeping the controller awake + rail up when keep-alive holds it warm. } @@ -2955,8 +2805,10 @@ void handlePipeWriteStart(uint8_t* data, uint16_t len) { uint16_t dispW = globalConfig.displays[0].pixel_width; uint16_t dispH = globalConfig.displays[0].pixel_height; // 5: partial uses two 1bpp planes (old+new). FastEPD IT8951 accepts that stream - // and applies a row-band update; 2bpp+/E1004 remain unsupported. - if (getBitsPerPixel() != 1 || e1004_panel_used()) { + // and applies a row-band update; 2bpp+ remains unsupported. Dual-controller + // panels are bwgbry_split (4 bpp), so this excludes them without a clause of + // their own. + if (getBitsPerPixel() != 1) { displayed_etag = 0; sendPipeStartNack(OD_ERR_PIPE_START_PARTIAL_UNSUPPORTED); return; } // 6: etag gate — nonzero and must match what is currently on the panel. diff --git a/src/display_service.h b/src/display_service.h index cfb2a73..653c62a 100644 --- a/src/display_service.h +++ b/src/display_service.h @@ -23,12 +23,6 @@ void epdSessionTick(void); // millis()-poll from loop()/idleDelay(): expir bool epdSessionIsWarm(void); // true when the panel is powered-idle (PWR_WARM) bool fastepd_driver_used(void); -bool e1004_panel_used(void); -uint8_t e1004_cs2_pin(void); -bool e1004_begin_plane(void); -bool e1004_advance_to_cs2(void); -void e1004_end_plane(void); -void e1004_write_stream_bytes(const uint8_t* data, uint16_t len); int mapEpd(int id); bool waitforrefresh(int timeout); float readBatteryVoltage(); diff --git a/src/split_panel.cpp b/src/split_panel.cpp new file mode 100644 index 0000000..57cb646 --- /dev/null +++ b/src/split_panel.cpp @@ -0,0 +1,224 @@ +#include "split_panel.h" + +#include +#include +#include +#include "structs.h" +#include "od_log.h" +#include "watchdog.h" + +extern struct GlobalConfig globalConfig; +extern BBEPDISP bbep; + +// bb_epaper's public header forward-declares only bbepWriteCmd/bbepWriteData/ +// bbepCMD2; everything else is declared by its callers (display_service.cpp does +// the same). These are compiled into the library from bb_ep.inl / arduino_io.inl. +void bbepSetCS2(BBEPDISP *pBBEP, uint8_t cs); +void bbepInitIO(BBEPDISP *pBBEP, uint8_t u8DC, uint8_t u8RST, uint8_t u8BUSY, uint8_t u8CS, + uint8_t u8MOSI, uint8_t u8SCK, uint32_t u32Speed); +void bbepWaitBusy(BBEPDISP *pBBEP); +bool bbepIsBusy(BBEPDISP *pBBEP); + +// Tripwires. splitPanelUsed() deliberately does NOT track a library define, so +// these assert that the pieces we build on still exist. If bb_epaper reworks the +// split-buffer model again this fails to compile on every target, rather than +// quietly producing firmware that cannot drive the panel — which is exactly how +// the BBEP_T133A01-gated predecessor of this file disappeared. +static_assert((int)EP133_SPECTRA_1200x1600 > 0, + "bb_epaper: EP133_SPECTRA_1200x1600 missing/renamed (was EP133A_* in the limengdu fork)"); +static_assert(BBEP_SPLIT_BUFFER == 0x0400, + "bb_epaper: BBEP_SPLIT_BUFFER changed; split-panel dispatch must be revisited"); +static_assert(CMD_CS_NONE == 0xf8 && CMD_CS1 == 0xfb, + "bb_epaper: cs_mode opcodes changed; the manual chip-select sequence must be revisited"); + +static bool s_geometryOk = false; +static bool s_streamOpen = false; +static bool s_onLeftHalf = true; +static uint32_t s_halfWritten = 0; // bytes into the half currently selected +static bool s_faulted = false; + +// Spectra6 accepts colour codes 0-3, 5 and 6 per nibble; 4 and 7-15 are undefined +// and a panel fed one can latch an unintended waveform. bb_epaper sanitises at +// DRAW time via u8Colors_spectra, but this firmware never draws — it forwards +// host-packed bytes — so the clamp has to happen on the way to the wire. +static uint8_t s_nibbleLut[256]; +static bool s_lutReady = false; + +static void buildNibbleLut(void) { + if (s_lutReady) return; + for (int i = 0; i < 256; i++) { + const uint8_t hi = (uint8_t)((i >> 4) & 0x0f); + const uint8_t lo = (uint8_t)(i & 0x0f); + const uint8_t hiOk = (hi <= 3 || hi == 5 || hi == 6) ? hi : 0; + const uint8_t loOk = (lo <= 3 || lo == 5 || lo == 6) ? lo : 0; + s_nibbleLut[i] = (uint8_t)((hiOk << 4) | loOk); + } + s_lutReady = true; +} + +// One controller's share of the frame: half-width rows at 4bpp, full height. +static inline uint32_t halfPlaneBytes(void) { + return ((uint32_t)bbep.native_width / 4u) * (uint32_t)bbep.native_height; +} + +bool splitPanelUsed(void) { + return (bbep.iFlags & BBEP_SPLIT_BUFFER) != 0; +} + +uint8_t splitPanelCs2Pin(void) { + uint8_t p = globalConfig.displays[0].cs_pin_2; + if (p == 0 || p == 0xFF) return 2; + return p; +} + +bool splitPanelGeometryOk(void) { + return s_geometryOk; +} + +void splitPanelConfigureGeometry(void) { + s_geometryOk = false; + s_streamOpen = false; + s_onLeftHalf = true; + s_halfWritten = 0; + s_faulted = false; + if (!splitPanelUsed()) return; + + const DisplayConfig& d = globalConfig.displays[0]; + if (d.pixel_width != bbep.native_width || d.pixel_height != bbep.native_height || + d.color_scheme != OD_COLOR_SCHEME_BWGBRY_SPLIT) { + od_log_error("ERROR: split panel requires a %dx%d bwgbry_split (8) display config", + bbep.native_width, bbep.native_height); + return; + } + s_geometryOk = true; +} + +void splitPanelInitIo(void) { + const DisplayConfig& d = globalConfig.displays[0]; + // CS2 before initIO: bbepInitIO() finishes by sending the panel's init + // sequence, whose CMD_CS1_CS2 opcodes need iCS2Pin already assigned or the + // shared registers reach only the primary controller. + bbepSetCS2(&bbep, splitPanelCs2Pin()); + odWatchdogBreadcrumb(OD_WDT_PHASE_INIT_SEQ); + odWatchdogFeed(); // bbepInitIO sends pInitFull internally (~240 s worst case) + bbepInitIO(&bbep, d.dc_pin, d.reset_pin, d.busy_pin, d.cs_pin, d.data_pin, d.clk_pin, 8000000); +} + +// bbepWriteCmd()/bbepWriteCmdData() open with `if (!is_awake) bbepWakeUp()`, and +// bbepWakeUp() toggles RST -- a hardware reset that would wipe the controller RAM +// we are part-way through filling. By the time any of this runs the panel really +// is awake (bbepInitIO -> bbepSendCMDSequence set the flag), so a zero here can +// only be stale bookkeeping; assert it rather than let it reset us mid-frame. +static inline void assumeAwake(void) { bbep.is_awake = 1; } + +// Select one controller, or neither. cs_mode must already be CMD_CS_NONE, which +// is what stops the library toggling CS underneath us on every write. +static inline void selectControllers(bool cs1, bool cs2) { + digitalWrite(bbep.iCSPin, cs1 ? LOW : HIGH); // upstream: iCSPin IS CS1 (iCS1Pin was deleted) + digitalWrite(bbep.iCS2Pin, cs2 ? LOW : HIGH); +} + +bool splitPanelBeginFrame(void) { + if (s_streamOpen) splitPanelCloseFrame(); + s_onLeftHalf = true; + s_halfWritten = 0; + s_faulted = false; + if (!splitPanelUsed() || !s_geometryOk) return false; + + // Never start blasting a half-plane into a controller that is still finishing + // the previous refresh. + if (bbepIsBusy(&bbep)) bbepWaitBusy(&bbep); + + assumeAwake(); + bbep.cs_mode = CMD_CS_NONE; + selectControllers(true, false); + bbepWriteCmd(&bbep, UC8151_DTM1); // 0x10; also leaves DC HIGH == data mode + s_streamOpen = true; + return true; +} + +// Cross to the second controller: releasing CS1 ends its burst, and the second +// DTM opens the right half-plane. +static bool advanceToRightHalf(void) { + if (!s_streamOpen || !s_onLeftHalf) return false; + selectControllers(false, true); + assumeAwake(); + bbepWriteCmd(&bbep, UC8151_DTM1); + s_onLeftHalf = false; + s_halfWritten = 0; + return true; +} + +void splitPanelSinkBytes(const uint8_t* data, uint32_t len) { + if (!data || len == 0) return; + if (!s_streamOpen) { s_faulted = true; return; } + buildNibbleLut(); + + const uint32_t half = halfPlaneBytes(); + // 512 B: one bbepWriteData -> SPI.transferBytes per chunk. Large enough to + // amortise the per-call overhead across a ~480 KB half-plane, and it swallows + // the boot screen's 300-byte half-rows whole. + uint8_t scratch[512]; + + while (len > 0) { + if (s_halfWritten >= half) { + if (s_onLeftHalf) { + if (!advanceToRightHalf()) { s_faulted = true; return; } + } else { + // More image than the panel has pixels. The bufferless shim this + // replaces dropped these silently and still reported success. + od_log_warn("Split panel: %lu excess image bytes dropped", (unsigned long)len); + s_faulted = true; + return; + } + } + // uint32_t throughout: the predecessor computed this run length in a + // uint16_t, so any chunk of 65536 bytes truncated to zero and spun forever. + uint32_t run = half - s_halfWritten; + if (run > len) run = len; + if (run > sizeof(scratch)) run = (uint32_t)sizeof(scratch); + + for (uint32_t i = 0; i < run; i++) scratch[i] = s_nibbleLut[data[i]]; + bbepWriteData(&bbep, scratch, (int)run); // CS_NONE: pure byte burst, no CS, no DC + + data += run; + len -= run; + s_halfWritten += run; + } +} + +bool splitPanelCloseFrame(void) { + if (!s_streamOpen) return false; + selectControllers(false, false); + // Unconditional restore, matching bbepWriteImage4bppDual's epilogue: handing + // CMD_CS_NONE back to library code that expects to drive CS itself means its + // writes reach no controller, silently. + bbep.cs_mode = CMD_CS1; + s_streamOpen = false; + + const bool complete = !s_onLeftHalf && s_halfWritten >= halfPlaneBytes(); + if (s_faulted) { + od_log_error("ERROR: split panel frame faulted; refusing to refresh"); + return false; + } + if (!complete) { + // Short frame. The caller's byte-count check catches a client that + // under-declares its total, but not a frame cut short by a geometry + // mismatch or a dropped transport. + od_log_error("ERROR: split panel frame short (half %d, %lu of %lu bytes)", + s_onLeftHalf ? 0 : 1, (unsigned long)s_halfWritten, + (unsigned long)halfPlaneBytes()); + return false; + } + return true; +} + +void splitPanelPowerOff(void) { + if (!splitPanelUsed()) return; + // POFF to both controllers, then wait for the HV rails to bleed down before + // anything considers cutting VDD. + bbep.cs_mode = CMD_CS1_CS2; + bbepCMD2(&bbep, UC8151_POFF, 0x00); + bbepWaitBusy(&bbep); + bbep.cs_mode = CMD_CS1; +} diff --git a/src/split_panel.h b/src/split_panel.h new file mode 100644 index 0000000..eb35e21 --- /dev/null +++ b/src/split_panel.h @@ -0,0 +1,92 @@ +#ifndef SPLIT_PANEL_H +#define SPLIT_PANEL_H + +#include +#include + +// Dual-controller ("split buffer") e-paper panels — a single glass driven by two +// controllers on separate chip-selects, each owning one half of every row. +// Currently the Seeed reTerminal E1004's 13.3" 1200x1600 Spectra6 (T133A01) and +// the 8.1" 1024x576 Spectra6. +// +// Selection is entirely at RUNTIME: splitPanelUsed() tests BBEP_SPLIT_BUFFER on +// bbep.iFlags, which bbepSetPanelType() fills in from the panel table for whatever +// panel_ic_type the device config names. There is no build flag — this code is +// compiled into every target, and a device that is not configured for a split +// panel simply never enters it. The predecessor of this file was gated on +// BBEP_T133A01, a *bb_epaper* define rather than a firmware one, so repinning the +// library silently compiled the whole panel away while the build still succeeded. +// +// BUFFERLESS. Image bytes go straight from the transport to SPI; nothing here +// allocates a framebuffer. bb_epaper's own dual-controller writer +// (bbepWriteImage4bppDual, reached via bbepWritePlane) requires a 960 KB +// ucScreen, so this drives the data phase directly from the library's CS +// primitives instead — cs_mode = CMD_CS_NONE, manual chip-select, one DTM per +// controller, chunked bbepWriteData. That is the same idiom bbepWriteImage4bppDual +// uses internally; only the byte source differs. +// +// The other three phases need no help: the panel's init sequence carries +// CMD_CS1/CMD_CS1_CS2 opcodes, and bbepRefresh()/bbepSleep() broadcast DRF/POFF to +// both controllers off the BBEP_SPLIT_BUFFER flag whether or not a buffer exists. +// +// The limengdu bb_epaper fork exported bbepStartDataStream/WriteDataStreamByte/ +// EndDataStream for exactly this; upstream removed them, which is what this +// module replaces. + +/** True when the configured panel is dual-controller. Valid once bbepSetPanelType() has run. */ +bool splitPanelUsed(void); + +/** CS2 GPIO from the device config (displays[0].cs_pin_2), defaulting to 2. */ +uint8_t splitPanelCs2Pin(void); + +/** + * Cold bring-up: set CS2, then bbepInitIO(), which sends the panel's init + * sequence — whose embedded CMD_CS1_CS2 opcodes need CS2 already configured, hence + * the ordering. + */ +void splitPanelInitIo(void); + +/** + * Validate config geometry against the panel. Call from + * configureBbepPanelGeometry() after bbepSetPanelType(). Sets the flag read by + * splitPanelGeometryOk(). + */ +void splitPanelConfigureGeometry(void); + +/** True when config dimensions and colour scheme match the panel. */ +bool splitPanelGeometryOk(void); + +/** + * Open the frame: wait out any live refresh, take manual control of both chip + * selects, and send the first DTM to the primary controller. Every exit path owes + * a splitPanelCloseFrame() — leaving cs_mode at CMD_CS_NONE makes every + * subsequent library write silently reach no controller at all. + */ +bool splitPanelBeginFrame(void); + +/** + * Stream image bytes in OD_COLOR_SCHEME_BWGBRY_SPLIT wire order — the entire left + * half-plane (h rows of native_width/4 bytes) followed by the entire right + * half-plane. Crossing to the second controller happens here, at the half + * boundary. Bytes past the end of the frame are dropped and the frame is flagged. + */ +void splitPanelSinkBytes(const uint8_t* data, uint32_t len); + +/** + * Release both chip selects and restore cs_mode. Idempotent, and safe to call on + * teardown paths that never opened a frame. Returns true only when a complete, + * unfaulted frame was streamed — callers refreshing the panel should treat false + * as "do not refresh", so a short or overrun frame cannot reach the glass as a + * successful update. + */ +bool splitPanelCloseFrame(void); + +/** + * Power both controllers down after a refresh has completed. bb_epaper's C + * bbepRefresh() issues DRF and returns without waiting or powering off (only the + * C++ BBEPAPER::refresh wrapper sends POFF, and this firmware does not use it), + * so without this the HV rails stay up for the whole keep-alive window. + */ +void splitPanelPowerOff(void); + +#endif From be5f8c1c92ca072d979b1a1a14ce99e4774d494b Mon Sep 17 00:00:00 2001 From: David Lee <247393336+davelee98@users.noreply.github.com> Date: Sun, 9 Aug 2026 16:28:47 -0400 Subject: [PATCH 3/5] build: add esp32-s3-N32R8-extuart-debug bench env Debug-level logging on the E1004's hardware. Mirrors esp32-s3-N16R8-extuart-debug: extends the parent env and adds -DOD_LOG_LEVEL=OD_LOG_DEBUG, nothing else. Deliberately absent from default_envs and .github/firmware-targets.json, as both existing -debug envs are -- this is a bench build, not a shipped target, so CI stays at 11 and the release matrix is unchanged. Worth having on this env specifically: -extuart routes logging to GPIO43/44 and prints no panic backtrace over USB, so debug logs are the only running commentary during dual-controller panel bring-up. +12,116 B flash, +16 B RAM over the parent (in line with the N16R8 pair's +12,492). All 16 envs build. --- platformio.ini | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 9e8d6f9..bb77f4f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -306,10 +306,21 @@ board_upload.maximum_ram_size = 327680 board_upload.flash_size = 32MB monitor_speed = 115200 +; Debug logging on the same hardware. Not in default_envs and not in +; .github/firmware-targets.json -- like esp32-s3-N16R8-extuart-debug, this is a +; bench build, not a shipped target. Worth having here specifically because +; -extuart builds print no panic backtrace over USB (logging goes out GPIO43/44), +; so debug-level logs are the only running commentary during panel bring-up. +[env:esp32-s3-N32R8-extuart-debug] +extends = env:esp32-s3-N32R8-extuart +build_flags = + ${env:esp32-s3-N32R8-extuart.build_flags} + -DOD_LOG_LEVEL=OD_LOG_DEBUG + ; The Seeed reTerminal E1004 has no env of its own: it is the same hardware as ; esp32-s3-N32R8-extuart, and its 13.3" Spectra6 panel is selected at runtime by ; panel_ic_type in the device config, not at build time. Flash it with -; esp32-s3-N32R8-extuart. +; esp32-s3-N32R8-extuart (or the -debug env above for bring-up). ; ESP targets pin pioarduino 55.03.39 (Arduino 3.3.9 / IDF 5.5.4), by exact version ; so it cannot drift CI vs local. From c91282b0e42bfdcd8a70c5e9806f357047e1494c Mon Sep 17 00:00:00 2001 From: David Lee <247393336+davelee98@users.noreply.github.com> Date: Sun, 9 Aug 2026 18:39:23 -0400 Subject: [PATCH 4/5] fix(power): drive cs_pin_2 low when the panel rail is cut configureDisplayPinsLowPower() drove cs_pin, clk, data, dc and reset low on every pwrmgm(false) but left cs_pin_2 out, so on dual-controller panels the second chip select stayed a driven output at whatever level the last frame left it (HIGH) while the rail went away -- the one panel pin still able to source into an unpowered controller through its protection diodes. Guarded separately rather than added to the pins[] array: that loop skips only 0xFF, and an unconfigured cs_pin_2 reads 0 on most devices, which would drive GPIO0 -- a strapping pin -- low on the way down. The 0/0xFF guard matches what the auxPins loop below already does. Keyed on the pin being configured, not on splitPanelUsed(): main.cpp has no business knowing the panel class, and a configured cs_pin_2 means a second controller regardless. Scope: this closes the pin state at rail cut, not through deep sleep. Nothing calls gpio_hold_en on the panel bus (only the power-latch pin is held), so every one of these pads still reverts at sleep entry. That is a separate, board-wide question needing the schematic or an ammeter. --- src/main.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index 4cfab10..77d0031 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1241,6 +1241,19 @@ static void configureDisplayPinsLowPower() { pinMode(pin, OUTPUT); digitalWrite(pin, LOW); } + // Second chip select on dual-controller panels (T133A01 / 8.1" Spectra6). + // Not in the array above: that loop only skips 0xFF, and an unconfigured + // cs_pin_2 reads 0 on most devices -- which would drive GPIO0, a strapping + // pin, LOW on the way to sleep. Guarded like the aux pins below instead. + // + // Left out until now, so CS2 stayed a driven output at whatever level the + // last frame left it (HIGH) while the rail went away -- the one panel pin + // still able to source into an unpowered controller through its protection + // diodes. LOW matches what cs_pin already does. + if (d.cs_pin_2 != 0xFF && d.cs_pin_2 != 0) { + pinMode(d.cs_pin_2, OUTPUT); + digitalWrite(d.cs_pin_2, LOW); + } if (d.busy_pin != 0xFF) { pinMode(d.busy_pin, INPUT); } From 218a8e9ca52dff1e83fd6728dc33422f55f7d9d1 Mon Sep 17 00:00:00 2001 From: David Lee <247393336+davelee98@users.noreply.github.com> Date: Sun, 9 Aug 2026 18:41:31 -0400 Subject: [PATCH 5/5] docs: correct env counts after dropping the esp32-s3-E1004 target CI builds 11 environments, not 12, and default_envs lists 10, not 11 -- esp32-s3-E1004 is gone (same hardware as esp32-s3-N32R8-extuart; the panel is selected at runtime by panel_ic_type). The esp32-wrover-e-N4R8 caveat is unchanged: it still ships and is still absent from default_envs. --- CLAUDE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 75a0bcf..b642389 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,8 +23,8 @@ pio run # build every environment Common envs: `nrf52840custom`, `esp32-s3-N16R8`, `esp32-s3-N8R8`, `esp32-c3-N16`, `esp32-c6-N4`. CI (`.github/workflows/main.yaml`) builds every -environment in `.github/firmware-targets.json` on every push — **12** of them — -keep them all green. Note `platformio.ini`'s `default_envs` lists only 11: +environment in `.github/firmware-targets.json` on every push — **11** of them — +keep them all green. Note `platformio.ini`'s `default_envs` lists only 10: `esp32-wrover-e-N4R8` ships but is NOT in it, so a bare `pio run` silently skips the target most likely to catch a broken `#ifndef OPENDISPLAY_HAS_WIFI` path. Build it explicitly (`pio run -e esp32-wrover-e-N4R8`) before claiming a