Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,39 @@ board_upload.flash_size = 32MB
monitor_speed = 115200
lib_ignore = Seeed_GFX

; Seeed reTerminal E1004 — ESP32-S3R8 (8 MB OPI PSRAM, 32 MB QSPI flash, CH343P
; UART console on GPIO43/44) driving the 13.3" T133A01 Spectra 6 dual-controller
; panel (1200x1600). Panel/bus pins come from runtime device config.
[env:esp32-s3-E1004]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip
framework = arduino
; bb_epaper pinned to the bitbank2/bb_epaper#32 commit; drop the pin once merged.
lib_deps =
https://github.com/limengdu/bb_epaper.git#95fd94afe39cd7db32bef7c70eea06d654264ff6
build_flags =
-DTARGET_ESP32
; 32 KB inflate window: clients (py-opendisplay / HA) compress with standard zlib.
-DOPENDISPLAY_ZLIB_WINDOW_BITS=15
-DOPENDISPLAY_ZLIB_USE_HEAP_WINDOW=1
-DBOARD_HAS_PSRAM
-DARDUINO_USB_MODE=0
-DARDUINO_USB_CDC_ON_BOOT=0
-DCONFIG_FREERTOS_WATCHDOG_TIMEOUT_S=120
-DOPENDISPLAY_LOG_UART
-DOPENDISPLAY_LOG_UART_RX=44
-DOPENDISPLAY_LOG_UART_TX=43
board_build.filesystem = littlefs
board = esp32-s3-devkitc-1
board_build.partitions = default_32MB.csv
board_build.flash_mode = qio
board_build.arduino.memory_type = qio_opi
board_build.psram_type = qspi_opi
board_upload.maximum_size = 33554432
board_upload.maximum_ram_size = 327680
board_upload.flash_size = 32MB
monitor_speed = 115200
lib_ignore = Seeed_GFX

[env:esp32-c3-N4]
platform = espressif32
framework = arduino
Expand Down
26 changes: 23 additions & 3 deletions src/boot_screen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ void writeSerial(String message, bool newLine);
void bbepSetAddrWindow(BBEPDISP *pBBEP, int x, int y, int cx, int cy);
void bbepStartWrite(BBEPDISP *pBBEP, int iPlane);
void bbepWriteData(BBEPDISP *pBBEP, uint8_t *pData, int iLen);
int bbepWritePlane(BBEPDISP *pBBEP, int iPlane, int bInvert);

typedef struct { char c; uint8_t col[5]; } BootGlyph5x7;
static const BootGlyph5x7 BOOT_FONT5X7[] = {
Expand Down Expand Up @@ -878,6 +879,14 @@ bool writeBootScreenWithQr() {
int textStartY = textY;
const uint16_t footerInfoY = (uint16_t)(footerY0 + (footerPadTop + footerInfoH - 7 * footerInfoScale) / 2);

// Dual-controller E1004: rows go to the framebuffer (same packed-4bpp
// pitch) and bbepWritePlane() sends them; rows cannot be streamed.
const bool e1004Framebuffered = e1004_panel_used();
if (e1004Framebuffered && bbep.ucScreen == NULL) {
writeSerial("Boot screen: E1004 framebuffer not allocated", true);
return false;
}

uint8_t* row = staticRowBuffer;
// bb_epaper 4-gray (scheme 5) needs the packed 2bpp image split into two
// 1-bit controller planes, so render the frame once per plane and
Expand All @@ -899,13 +908,15 @@ bool writeBootScreenWithQr() {
: (colorSwatchPlane1 ? (pass == 0 ? PLANE_0 : PLANE_1)
: (useBitplanes ? PLANE_0 : getplane()));
#if defined(TARGET_ESP32) && defined(OPENDISPLAY_SEEED_GFX)
if (!seeed_driver_used()) {
if (!seeed_driver_used() && !e1004Framebuffered) {
bbepSetAddrWindow(&bbep, 0, 0, w, h);
bbepStartWrite(&bbep, targetPlane);
}
#else
bbepSetAddrWindow(&bbep, 0, 0, w, h);
bbepStartWrite(&bbep, targetPlane);
if (!e1004Framebuffered) {
bbepSetAddrWindow(&bbep, 0, 0, w, h);
bbepStartWrite(&bbep, targetPlane);
}
#endif
const int ls = bootLineStep(middleScaleText);
const uint16_t domY = (uint16_t)textStartY;
Expand Down Expand Up @@ -975,19 +986,28 @@ bool writeBootScreenWithQr() {
seeed_gfx_boot_write_row(y_native, row, pitch);
} else if (gray4Split) {
writeGray4PlaneRow(row, pitch, planePitch, w, bitSel);
} else if (e1004Framebuffered) {
memcpy(&bbep.ucScreen[(size_t)y_native * pitch], row, pitch);
} else {
bbepWriteData(&bbep, row, pitch);
}
#else
if (gray4Split) {
writeGray4PlaneRow(row, pitch, planePitch, w, bitSel);
} else if (e1004Framebuffered) {
memcpy(&bbep.ucScreen[(size_t)y_native * pitch], row, pitch);
} else {
bbepWriteData(&bbep, row, pitch);
}
#endif
}
}

if (e1004Framebuffered) {
bbepWritePlane(&bbep, PLANE_0, 0);
writeSerial("Boot screen with QR rendered", true);
return true;
}
#if defined(TARGET_ESP32) && defined(OPENDISPLAY_SEEED_GFX)
if (seeed_driver_used()) {
seeed_gfx_boot_skip_planes();
Expand Down
158 changes: 133 additions & 25 deletions src/display_service.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ 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);
int bbepWritePlane(BBEPDISP *pBBEP, int iPlane, int bInvert);
int bbepAllocBuffer(BBEPDISP *pBBEP, int bDoubleSize);
#ifdef BBEP_T133A01 // bb_epaper build with T133A01 dual-controller support
void bbepSetCS2(BBEPDISP *pBBEP, uint8_t cs);
#endif
void flashLed(uint8_t color, uint8_t brightness);
bool waitforrefresh(int timeout);

Expand Down Expand Up @@ -149,8 +154,25 @@ static void prepareEpdRailForBoot() {
#endif
}

#ifdef BBEP_T133A01
// CS2 must be set before bbepInitIO(), which runs the full dual-chip init;
// the generic wakeup + re-init would reach only one controller.
static void e1004InitPanel(void) {
const DisplayConfig& d = globalConfig.displays[0];
bbepSetCS2(&bbep, e1004_cs2_pin());
bbepInitIO(&bbep, d.dc_pin, d.reset_pin, d.busy_pin, d.cs_pin, d.data_pin, d.clk_pin, 8000000);
}
#endif

static void initBbepPanelSession() {
const DisplayConfig& d = globalConfig.displays[0];
#ifdef BBEP_T133A01
if (e1004_panel_used()) {
e1004InitPanel();
delay(200);
return;
}
#endif
bbepInitIO(&bbep, d.dc_pin, d.reset_pin, d.busy_pin, d.cs_pin, d.data_pin, d.clk_pin, 8000000);
bbepWakeUp(&bbep);
bbepSendCMDSequence(&bbep, bbep.pInitFull);
Expand Down Expand Up @@ -246,12 +268,20 @@ static bool epdSessionAcquire(bool partialInit) {
pwrmgm(true); // -> PWR_ACTIVE (guarded; real transition)
if (!epdSessionUsesSeeed()) {
const DisplayConfig& d = globalConfig.displays[0];
bbepInitIO(&bbep, d.dc_pin, d.reset_pin, d.busy_pin, d.cs_pin, d.data_pin, d.clk_pin, 8000000);
bbepWakeUp(&bbep);
const uint8_t* initSeq = partialInit ? (bbep.pInitPart ? bbep.pInitPart : bbep.pInitFull)
: bbep.pInitFull;
bbepSendCMDSequence(&bbep, initSeq);
epdSessionInitWasPartial = partialInit;
#ifdef BBEP_T133A01
if (e1004_panel_used()) {
e1004InitPanel();
epdSessionInitWasPartial = false;
} else
#endif
{
bbepInitIO(&bbep, d.dc_pin, d.reset_pin, d.busy_pin, d.cs_pin, d.data_pin, d.clk_pin, 8000000);
bbepWakeUp(&bbep);
const uint8_t* initSeq = partialInit ? (bbep.pInitPart ? bbep.pInitPart : bbep.pInitFull)
: bbep.pInitFull;
bbepSendCMDSequence(&bbep, initSeq);
epdSessionInitWasPartial = partialInit;
}
}
cold = true;
} else {
Expand All @@ -263,11 +293,20 @@ 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 (!epdSessionUsesSeeed()) {
bbepWakeUp(&bbep);
const uint8_t* initSeq = partialInit ? (bbep.pInitPart ? bbep.pInitPart : bbep.pInitFull)
: bbep.pInitFull;
bbepSendCMDSequence(&bbep, initSeq);
epdSessionInitWasPartial = partialInit;
#ifdef BBEP_T133A01
if (e1004_panel_used()) {
// Warm T133A01 stays fully initialized (release never sleeps it);
// every transfer resends the frame and re-opens the RAM writes.
epdSessionInitWasPartial = false;
} else
#endif
{
bbepWakeUp(&bbep);
const uint8_t* initSeq = partialInit ? (bbep.pInitPart ? bbep.pInitPart : bbep.pInitFull)
: bbep.pInitFull;
bbepSendCMDSequence(&bbep, initSeq);
epdSessionInitWasPartial = partialInit;
}
}
cold = false;
}
Expand Down Expand Up @@ -477,8 +516,9 @@ int mapEpd(int id){
case 0x003F: return EP31_240x320;
case 0x0040: return EP75YR_800x480;
case 0x0041: return EP_PANEL_UNDEFINED;
// bb_epaper 2.1.9 does not define the 13.3" EP133 panel yet.
case 0x0042: return EP_PANEL_UNDEFINED;
#ifdef BBEP_T133A01
case PANEL_IC_EP133A_SPECTRA_1200X1600: return EP133A_SPECTRA_1200x1600; // 0x0042, Seeed reTerminal E1004
#endif
case 0x0043: return EP154_200x200_4GRAY;
case 0x0044: return EP42B_400x300_4GRAY;
case 0x0045: return EP397_800x480;
Expand Down Expand Up @@ -506,10 +546,34 @@ bool seeed_driver_used(void) {
#endif
}

// Seeed reTerminal E1004: 13.3" T133A01 Spectra 6, one controller per panel half.
bool e1004_panel_used(void) {
#ifdef BBEP_T133A01
if (globalConfig.display_count < 1) return false;
return globalConfig.displays[0].panel_ic_type == PANEL_IC_EP133A_SPECTRA_1200X1600;
#else
return false;
#endif
}

// Second chip select from the display block's spare reserved_pin_2 (cs_pin_2
// in the toolbox schema); 0 or 0xFF = board default GPIO2.
uint8_t e1004_cs2_pin(void) {
uint8_t p = globalConfig.displays[0].reserved_pin_2;
if (p == 0 || p == 0xFF) return 2;
return p;
}

bool waitforrefresh(int timeout){
#if defined(TARGET_ESP32) && defined(OPENDISPLAY_SEEED_GFX)
if (seeed_driver_used()) return seeed_gfx_wait_refresh(timeout);
#endif
if (e1004_panel_used() && !bbepIsBusy(&bbep)) {
// bbepRefresh() waits for BUSY internally on the T133A01; already idle
// here means the refresh completed, not that it never started.
writeSerial("Refresh completed inside bb_epaper", true);
return true;
}
// 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"
// error check stays valid at a 10 ms first poll. Loop bound scales x10
Expand Down Expand Up @@ -1355,10 +1419,31 @@ void initDisplay(){
#endif
{
prepareEpdRailForBoot();
uint8_t* prevFramebuffer = bbep.ucScreen; // E1004 frame survives re-init
memset(&bbep, 0, sizeof(BBEPDISP));
int panelType = mapEpd(globalConfig.displays[0].panel_ic_type);
bbepSetPanelType(&bbep, panelType);
bbepSetRotation(&bbep, globalConfig.displays[0].rotation * 90);
int rotation = globalConfig.displays[0].rotation * 90;
if (e1004_panel_used()) {
// The framebuffer is filled in native layout (renderers and host
// apply the configured rotation), so bb_epaper must not rotate.
rotation = 0;
// 1200x1600 4bpp PSRAM frame sent by bbepWritePlane(); the row
// streaming paths cannot split rows across the two chip selects.
// The writers derive sizes from the config, so refuse a config
// that does not match the panel: ucScreen stays NULL and the
// boot screen / transfer end paths fail instead of corrupting RAM.
if (globalConfig.displays[0].pixel_width != bbep.native_width ||
globalConfig.displays[0].pixel_height != bbep.native_height ||
globalConfig.displays[0].color_scheme != COLOR_SCHEME_BWGBRY) {
writeSerial("ERROR: E1004 requires a 1200x1600 BWGBRY display config", true);
} else if (prevFramebuffer != NULL) {
bbep.ucScreen = prevFramebuffer;
} else if (bbepAllocBuffer(&bbep, 0) != BBEP_SUCCESS) {
writeSerial("ERROR: E1004 framebuffer allocation failed", true);
}
}
bbepSetRotation(&bbep, rotation);
writeSerial(String("Height: ") + String(globalConfig.displays[0].pixel_height), true);
writeSerial(String("Width: ") + String(globalConfig.displays[0].pixel_width), true);
initBbepPanelSession();
Expand Down Expand Up @@ -1710,6 +1795,21 @@ static void streamGray4Bytes(const uint8_t* buf, uint32_t len) {
}
}

// Direct-write byte sink: the E1004 collects the frame in the framebuffer for
// bbepWritePlane(); other panels stream straight to panel RAM. Advances
// directWriteBytesWritten by the full length either way (callers' accounting).
static void directWriteSinkBytes(uint8_t* data, uint32_t len) {
if (e1004_panel_used()) {
if (bbep.ucScreen != NULL && directWriteBytesWritten < directWriteTotalBytes) {
uint32_t space = directWriteTotalBytes - directWriteBytesWritten;
memcpy(&bbep.ucScreen[directWriteBytesWritten], data, (len < space) ? len : space);
}
} else {
bbepWriteData(&bbep, data, (int)len);
}
directWriteBytesWritten += len;
}

static bool directWriteTouchSuspended = false;

void cleanupDirectWriteState(bool refreshDisplay) {
Expand Down Expand Up @@ -1789,7 +1889,8 @@ static void directWriteActivatePanel(void) {
seeed_gfx_direct_write_reset();
} else
#endif
{
// E1004 frames go to the framebuffer; bbepWritePlane() opens the RAM writes.
if (!e1004_panel_used()) {
bbepSetAddrWindow(&bbep, 0, 0, globalConfig.displays[0].pixel_width, globalConfig.displays[0].pixel_height);
bbepStartWrite(&bbep, directWriteBitplanes ? PLANE_0 : getplane());
}
Expand Down Expand Up @@ -1869,7 +1970,7 @@ 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) {
if (getBitsPerPixel() != 1 || e1004_panel_used()) {
// 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.
Expand Down Expand Up @@ -1974,8 +2075,7 @@ void handleDirectWriteData(uint8_t* data, uint16_t len) {
if (directWriteIsGray4() || directWriteBitplanes) {
streamGray4Bytes(data, bytesToWrite); // advances directWriteBytesWritten, splits planes
} else {
bbepWriteData(&bbep, data, bytesToWrite);
directWriteBytesWritten += bytesToWrite;
directWriteSinkBytes(data, bytesToWrite);
}
}
imageWriteLogProgress(directWriteBytesWritten, directWriteTotalBytes);
Expand Down Expand Up @@ -2056,6 +2156,8 @@ 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;
// T133A01 fast refresh falls back to re-init, whose reset wipes the frame.
if (e1004_panel_used()) refreshMode = REFRESH_FULL;
writeSerial("EPD refresh: ", false);
writeSerial(refreshMode == REFRESH_FAST ? "FAST" : "FULL", false);
writeSerial(" (mode=", false);
Expand Down Expand Up @@ -2084,8 +2186,16 @@ static void directWriteFinishAndRefresh(uint8_t* data, uint16_t len, uint8_t end
} else
#endif
{
bbepRefresh(&bbep, refreshMode);
refreshSuccess = waitforrefresh(60);
bool planeOk = true;
if (e1004_panel_used()) {
// Send the framebuffer to both controllers before the refresh;
// fails when the framebuffer is absent (bad config / alloc failure).
planeOk = (bbepWritePlane(&bbep, PLANE_0, 0) == BBEP_SUCCESS);
}
if (planeOk) {
bbepRefresh(&bbep, refreshMode);
refreshSuccess = waitforrefresh(60);
}
// No bbepSleep here: cleanupDirectWriteState(false) releases the session,
// keeping the controller awake + rail up when keep-alive holds it warm.
}
Expand Down Expand Up @@ -2261,8 +2371,7 @@ static bool pipeConsumePayload(uint8_t* data, uint16_t len) {
if (directWriteIsGray4() || directWriteBitplanes) {
streamGray4Bytes(data, toWrite); // advances directWriteBytesWritten, splits planes
} else {
bbepWriteData(&bbep, data, toWrite);
directWriteBytesWritten += toWrite;
directWriteSinkBytes(data, toWrite);
}
}
imageWriteLogProgress(directWriteBytesWritten, directWriteTotalBytes);
Expand Down Expand Up @@ -2315,7 +2424,7 @@ 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: two 1bpp controller planes are the partial mechanism; seeed/IT8951 has no equivalent.
if (getBitsPerPixel() != 1 || seeed_driver_used()) {
if (getBitsPerPixel() != 1 || seeed_driver_used() || e1004_panel_used()) {
displayed_etag = 0; sendPipeStartNack(0x06); return;
}
// 6: etag gate — nonzero and must match what is currently on the panel.
Expand Down Expand Up @@ -2774,8 +2883,7 @@ static bool zlib_stream_to_direct_write(const uint8_t* data, uint32_t len, bool
}
} else
{
bbepWriteData(&bbep, decompressionChunk, bytesOut);
directWriteBytesWritten += (uint32_t)bytesOut;
directWriteSinkBytes(decompressionChunk, (uint32_t)bytesOut);
}
if (directWriteBytesWritten > directWriteDecompressedTotal) {
return false;
Expand Down
3 changes: 3 additions & 0 deletions src/display_service.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ void epdSessionTick(void); // millis()-poll from loop()/idleDelay(): expir
bool epdSessionIsWarm(void); // true when the panel is powered-idle (PWR_WARM)

bool seeed_driver_used(void);
/** True when displays[0] is the E1004 13.3" T133A01 dual-controller panel. */
bool e1004_panel_used(void);
uint8_t e1004_cs2_pin(void);
int mapEpd(int id);
bool waitforrefresh(int timeout);
float readBatteryVoltage();
Expand Down
Loading