From ccdc4838368098e9accea77a3eabf5756619c5c7 Mon Sep 17 00:00:00 2001 From: Niels Timmer Date: Tue, 31 Mar 2026 16:26:37 +0200 Subject: [PATCH 1/6] Fix OTA auto-update UI stuck when ESP32 can't serve HTTP during flash The ESP32 is too busy writing flash to respond to /ota/status poll requests, so _autoOtaProgress often stays at a low value (e.g. 25%) even when the download completes successfully. The previous >= 90% threshold guard then blocked waitForReboot(), leaving the UI frozen. Replace the progress threshold with an _otaInstallStarted flag set after the server confirms the OTA task has begun. Any 404/network error after install is confirmed now correctly triggers waitForReboot(), regardless of the last polled progress value. --- src/web_server.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/web_server.cpp b/src/web_server.cpp index 319e66d..be94f12 100644 --- a/src/web_server.cpp +++ b/src/web_server.cpp @@ -1077,6 +1077,7 @@ function startOta(){ R"rawliteral( var _autoOtaUrl=''; var _autoOtaProgress=0; +var _otaInstallStarted=false; function switchFwTab(t){ document.getElementById('fw-tab-auto').style.display=t==='auto'?'block':'none'; document.getElementById('fw-tab-manual').style.display=t==='manual'?'block':'none'; @@ -1156,11 +1157,13 @@ function installUpdate(){ document.getElementById('autoOtaStatus').style.color='#8B949E'; document.getElementById('autoOtaStatus').textContent='Starting...'; _autoOtaProgress=0; + _otaInstallStarted=false; var p=new URLSearchParams();p.append('url',_autoOtaUrl); fetch('/ota/auto',{method:'POST',body:p}) .then(function(r){return r.json();}) .then(function(d){ if(d.error){throw new Error(d.error);} + _otaInstallStarted=true; pollOtaStatus(); }) .catch(function(e){ @@ -1191,9 +1194,11 @@ function pollOtaStatus(){ st.textContent=d.status+' ('+d.progress+'%)'; } }).catch(function(){ - // Device went offline or /ota/status no longer exists (rebooted to new firmware). - // If download reached >=90%, treat as success — HTTPUpdate doesn't guarantee a 100% tick. - if(_autoOtaProgress>=90){ + // Device went offline or /ota/status no longer exists (new firmware has no /ota/status). + // The ESP32 can't serve HTTP while writing flash, so _autoOtaProgress may be low even on + // success. Use _otaInstallStarted flag (set after server confirmed the install began) + // instead of a progress threshold to avoid getting stuck. + if(_otaInstallStarted){ clearInterval(_otaPoller);_otaPoller=null; var bar=document.getElementById('autoOtaBar'); var st=document.getElementById('autoOtaStatus'); From 0946c17c6c383356bb1aeece671168ddeaaecf1d Mon Sep 17 00:00:00 2001 From: Niels Timmer Date: Tue, 31 Mar 2026 16:43:47 +0200 Subject: [PATCH 2/6] =?UTF-8?q?Optimize=20ESP32-C3=20flash:=2095%=20?= =?UTF-8?q?=E2=86=92=2062.7%,=20new=20partition=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit platformio.ini (esp32c3 env): - Switch to partitions_4mb_c3.csv (1.875 MB OTA partitions) - build_unflags: strip LOAD_GFXFF and SMOOTH_FONT — GFX free fonts and smooth VLW font renderer are compiled in by default but never used - Explicit lib_deps without TFT_eWidget (dead code for C3) partitions_4mb_c3.csv (new): - app0/app1: 0x1E0000 (1.875 MB) each, up from 1.75 MB - SPIFFS: 0x30000 (192 KB), reduced from 448 KB — SPIFFS unused in BambuHelper - Result: 65 KB → 733 KB headroom - Note: partition table change requires USB re-flash; OTA updates thereafter src/main.cpp, src/display_ui.cpp: - USB CDC startup delay and Serial.flush() removal (previously unstaged) --- partitions_4mb_c3.csv | 11 +++++++++++ platformio.ini | 12 ++++++++++-- src/display_ui.cpp | 1 - src/main.cpp | 3 +++ 4 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 partitions_4mb_c3.csv diff --git a/partitions_4mb_c3.csv b/partitions_4mb_c3.csv new file mode 100644 index 0000000..632953b --- /dev/null +++ b/partitions_4mb_c3.csv @@ -0,0 +1,11 @@ +# Partition table for 4 MB flash (ESP32-C3 Super Mini) +# app partitions are 1.875 MB each — gives ~630 KB headroom over current firmware. +# SPIFFS is not used by BambuHelper (settings stored in NVS); kept small for padding. +# NOTE: changing the partition table requires a full USB flash; OTA cannot update it. +# +# Name, Type, SubType, Offset, Size, +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x1E0000, +app1, app, ota_1, 0x1F0000, 0x1E0000, +spiffs, data, spiffs, 0x3D0000, 0x30000, diff --git a/platformio.ini b/platformio.ini index c4bc7cf..101253a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -101,9 +101,17 @@ platform = espressif32 board = lolin_c3_mini framework = arduino monitor_speed = 115200 -board_build.partitions = partitions_4mb.csv +board_build.partitions = partitions_4mb_c3.csv extra_scripts = pre:scripts/patch_spi_for_esp32-c3.py -lib_deps = ${common.lib_deps} +; TFT_eWidget is not used — omit it from C3 to avoid pulling in dead code +lib_deps = + bodmer/TFT_eSPI@^2.5.43 + knolleary/PubSubClient@^2.8 + bblanchon/ArduinoJson@^7.0 +; Remove unused TFT font features inherited from common to reduce flash usage +build_unflags = + -D LOAD_GFXFF=1 + -D SMOOTH_FONT=1 build_flags = ${common.common_flags} -D SPI_FREQUENCY=40000000 diff --git a/src/display_ui.cpp b/src/display_ui.cpp index e48f1f0..a4ad76b 100644 --- a/src/display_ui.cpp +++ b/src/display_ui.cpp @@ -98,7 +98,6 @@ void initDisplay() { Serial.println("Display: pre-init delay..."); delay(500); Serial.println("Display: calling tft.init()..."); - Serial.flush(); tft.init(); // TFT_eSPI configures SPI from build flags Serial.println("Display: tft.init() done"); #if defined(DISPLAY_CYD) diff --git a/src/main.cpp b/src/main.cpp index b8a5f7d..333ceb5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -94,6 +94,9 @@ static void handleRotation() { // --------------------------------------------------------------------------- void setup() { Serial.begin(115200); +#if defined(ARDUINO_USB_CDC_ON_BOOT) + delay(2000); // Wait for USB CDC to re-enumerate after reset before printing +#endif Serial.printf("\n=== BambuHelper %s Starting ===\n", FW_VERSION); loadSettings(); From ab274a7581992de271145b0d2a36f56289a3fa17 Mon Sep 17 00:00:00 2001 From: Niels Timmer Date: Tue, 31 Mar 2026 17:12:48 +0200 Subject: [PATCH 3/6] Migrate display from TFT_eSPI to LovyanGFX across all boards Replace TFT_eSPI with LovyanGFX across the entire display stack. Board-specific configs (S3, CYD, C3) are C++ classes in display_ui.cpp. SPI pins and driver settings move from build_flags into the class configs, simplifying platformio.ini significantly. The C3 no longer needs the SPI patch script. Key changes: - Three LGFX board classes: LGFX_S3 (ST7789), LGFX_CYD (ILI9342), LGFX_C3 (ST7789) - fillArc replaces drawSmoothArc in gauge rendering; +90deg angle offset matches original gap-at-bottom orientation - alphaBlend565() helper replaces tft.alphaBlend() (not a member in LGFX) - drawArc (outline) replaces drawSmoothArc in animation code - S3 requires invertDisplay(true) at runtime for correct colors - Backlight handled via BACKLIGHT_PIN + analogWrite (unchanged) - config.h: BACKLIGHT_PIN guard added (was hardcoded to TFT_BL) --- include/config.h | 4 +- platformio.ini | 87 ++++-------------------- src/clock_pong.cpp | 3 - src/display_anim.cpp | 40 +++++------ src/display_anim.h | 10 +-- src/display_gauges.cpp | 56 +++++++++++----- src/display_gauges.h | 12 ++-- src/display_ui.cpp | 147 ++++++++++++++++++++++++++++++++++++++--- src/display_ui.h | 6 +- src/icons.h | 5 +- 10 files changed, 233 insertions(+), 137 deletions(-) diff --git a/include/config.h b/include/config.h index 790f6be..8621158 100644 --- a/include/config.h +++ b/include/config.h @@ -18,7 +18,9 @@ #include "layout.h" #define SCREEN_W LY_W #define SCREEN_H LY_H -#define BACKLIGHT_PIN TFT_BL // set by build flags per board +#ifndef BACKLIGHT_PIN +#define BACKLIGHT_PIN -1 // set via build_flags -D BACKLIGHT_PIN= +#endif #define BACKLIGHT_CH 0 #define BACKLIGHT_FREQ 5000 #define BACKLIGHT_RES 8 diff --git a/platformio.ini b/platformio.ini index 101253a..010bcab 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,28 +1,15 @@ ; BambuHelper - Bambu Lab Printer Monitor -; Supports ESP32-S3 Super Mini and ESP32-2432S028 (CYD) +; Supports ESP32-S3 Super Mini, ESP32-2432S028 (CYD), ESP32-C3 Super Mini ; --- Shared library dependencies --- [common] lib_deps = - bodmer/TFT_eSPI@^2.5.43 - bodmer/TFT_eWidget@^0.0.6 + lovyan03/LovyanGFX@^1.1.16 knolleary/PubSubClient@^2.8 bblanchon/ArduinoJson@^7.0 -; --- Shared TFT_eSPI + font flags (driver/pins are per-env) --- -common_flags = - -D USER_SETUP_LOADED=1 - -D DISABLE_ALL_LIBRARY_WARNINGS=1 - -D LOAD_GLCD=1 - -D LOAD_FONT2=1 - -D LOAD_FONT4=1 - -D LOAD_FONT6=1 - -D LOAD_FONT7=1 - -D LOAD_GFXFF=1 - -D SMOOTH_FONT=1 - ; ============================================================================= -; ESP32-S3 Super Mini + ST7789 240x240 +; ESP32-S3 Super Mini + ST7789 240x240 ; ============================================================================= [env:esp32s3] platform = espressif32 @@ -33,27 +20,15 @@ board_build.partitions = partitions_4mb.csv board_build.arduino.memory_type = qio_qspi lib_deps = ${common.lib_deps} build_flags = - ${common.common_flags} -D BOARD_VARIANT=\"esp32s3\" + -D BOARD_IS_S3=1 -D ENABLE_OTA_AUTO=1 - -D SPI_FREQUENCY=40000000 - ; --- Display driver --- - -D ST7789_DRIVER=1 - -D TFT_WIDTH=240 - -D TFT_HEIGHT=240 + -D BACKLIGHT_PIN=13 ; --- USB Serial (S3 native USB CDC) --- -D ARDUINO_USB_CDC_ON_BOOT=1 - ; --- SPI pins (S3 Super Mini) --- - -D TFT_MOSI=11 ;SDA - -D TFT_SCLK=12 ;SCL - -D TFT_CS=10 - -D TFT_DC=9 - -D TFT_RST=8 ;14 - -D TFT_BL=13 - -D USE_FSPI_PORT=1 ; ============================================================================= -; ESP32-2432S028 (CYD - Cheap Yellow Display) + ILI9341 240x320 +; ESP32-2432S028 (CYD - Cheap Yellow Display) + ILI9342 240x320 ; ============================================================================= [env:cyd] platform = espressif32 @@ -61,37 +36,23 @@ board = esp32dev framework = arduino monitor_speed = 115200 upload_speed = 230400 -board_build.partitions = partitions_4mb.csv +board_build.partitions = min_spiffs.csv lib_deps = ${common.lib_deps} https://github.com/PaulStoffregen/XPT2046_Touchscreen.git build_flags = - ${common.common_flags} - -D SPI_FREQUENCY=27000000 - ; --- Display driver --- - ; ILI9341_2 works for both ILI9341 and ILI9342 CYD variants - -D ILI9341_2_DRIVER=1 - -D TFT_INVERSION_ON=1 - -D TFT_WIDTH=240 - -D TFT_HEIGHT=320 - ; --- CYD display SPI pins --- - -D TFT_MOSI=13 - -D TFT_SCLK=14 - -D TFT_CS=15 - -D TFT_DC=2 - -D TFT_RST=12 - -D TFT_BL=21 ; --- CYD touch (XPT2046 on separate SPI bus) --- + -D USE_XPT2046=1 -D TOUCH_CS=33 -D TOUCH_IRQ=36 -D TOUCH_MOSI=32 -D TOUCH_MISO=39 -D TOUCH_CLK=25 -D SPI_TOUCH_FREQUENCY=2500000 - -D USE_XPT2046=1 ; --- Layout profile --- -D DISPLAY_CYD=1 -D BOARD_VARIANT=\"cyd\" + -D BACKLIGHT_PIN=21 ; ============================================================================= ; ESP32-C3 Super Mini + ST7789 240x240 @@ -102,31 +63,11 @@ board = lolin_c3_mini framework = arduino monitor_speed = 115200 board_build.partitions = partitions_4mb_c3.csv -extra_scripts = pre:scripts/patch_spi_for_esp32-c3.py -; TFT_eWidget is not used — omit it from C3 to avoid pulling in dead code -lib_deps = - bodmer/TFT_eSPI@^2.5.43 - knolleary/PubSubClient@^2.8 - bblanchon/ArduinoJson@^7.0 -; Remove unused TFT font features inherited from common to reduce flash usage -build_unflags = - -D LOAD_GFXFF=1 - -D SMOOTH_FONT=1 +lib_deps = ${common.lib_deps} build_flags = - ${common.common_flags} - -D SPI_FREQUENCY=40000000 - ; --- Display driver --- - -D ST7789_DRIVER=1 - -D TFT_WIDTH=240 - -D TFT_HEIGHT=240 - ; --- USB Serial (C3 native USB CDC) --- - -D ARDUINO_USB_CDC_ON_BOOT=1 - ; --- SPI pins (C3 Super Mini) --- - -D TFT_MOSI=20 ;SDA - -D TFT_SCLK=21 ;SCL - -D TFT_CS=6 - -D TFT_DC=7 - -D TFT_RST=10 - -D TFT_BL=5 -D BOARD_VARIANT=\"esp32c3\" + -D BOARD_IS_C3=1 -D ENABLE_OTA_AUTO=1 + -D BACKLIGHT_PIN=5 + ; --- USB Serial (C3 native USB CDC) --- + -D ARDUINO_USB_CDC_ON_BOOT=1 diff --git a/src/clock_pong.cpp b/src/clock_pong.cpp index c216d71..f5bf98d 100644 --- a/src/clock_pong.cpp +++ b/src/clock_pong.cpp @@ -11,11 +11,8 @@ #include "layout.h" #include "settings.h" #include "display_ui.h" -#include #include -extern TFT_eSPI tft; - // ========== Layout constants (from layout profile) ========== #define ARK_BRICK_ROWS LY_ARK_BRICK_ROWS #define ARK_BRICK_COLS LY_ARK_COLS diff --git a/src/display_anim.cpp b/src/display_anim.cpp index ba9c7c1..7ab12ee 100644 --- a/src/display_anim.cpp +++ b/src/display_anim.cpp @@ -12,19 +12,19 @@ // --------------------------------------------------------------------------- static uint16_t spinnerAngle = 0; -void drawSpinner(TFT_eSPI& tft, int16_t cx, int16_t cy, int16_t radius, +void drawSpinner(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, int16_t radius, uint16_t color) { // Erase previous arc segment (handle wrap-around) uint16_t prevStart = (spinnerAngle + 360 - 12) % 360; uint16_t prevEnd = (prevStart + 60) % 360; if (prevEnd > prevStart) { - tft.drawSmoothArc(cx, cy, radius, radius - 4, - prevStart, prevEnd, CLR_BG, CLR_BG, false); + tft.drawArc(cx, cy, radius, radius - 4, + prevStart, prevEnd, CLR_BG); } else { - tft.drawSmoothArc(cx, cy, radius, radius - 4, - prevStart, 360, CLR_BG, CLR_BG, false); - tft.drawSmoothArc(cx, cy, radius, radius - 4, - 0, prevEnd, CLR_BG, CLR_BG, false); + tft.drawArc(cx, cy, radius, radius - 4, + prevStart, 360, CLR_BG); + tft.drawArc(cx, cy, radius, radius - 4, + 0, prevEnd, CLR_BG); } // Advance angle @@ -34,20 +34,20 @@ void drawSpinner(TFT_eSPI& tft, int16_t cx, int16_t cy, int16_t radius, // Draw arc segment (handle wrap-around) if (arcEnd > arcStart) { - tft.drawSmoothArc(cx, cy, radius, radius - 4, - arcStart, arcEnd, color, CLR_BG, false); + tft.drawArc(cx, cy, radius, radius - 4, + arcStart, arcEnd, color); } else { - tft.drawSmoothArc(cx, cy, radius, radius - 4, - arcStart, 360, color, CLR_BG, false); - tft.drawSmoothArc(cx, cy, radius, radius - 4, - 0, arcEnd, color, CLR_BG, false); + tft.drawArc(cx, cy, radius, radius - 4, + arcStart, 360, color); + tft.drawArc(cx, cy, radius, radius - 4, + 0, arcEnd, color); } } // --------------------------------------------------------------------------- // Animated dots "..." // --------------------------------------------------------------------------- -void drawAnimDots(TFT_eSPI& tft, int16_t x, int16_t y, uint16_t color) { +void drawAnimDots(lgfx::LGFX_Device& tft, int16_t x, int16_t y, uint16_t color) { unsigned long ms = millis(); int phase = (ms / 400) % 4; @@ -64,7 +64,7 @@ void drawAnimDots(TFT_eSPI& tft, int16_t x, int16_t y, uint16_t color) { // --------------------------------------------------------------------------- // Indeterminate slide bar — a glowing segment slides back and forth // --------------------------------------------------------------------------- -void drawSlideBar(TFT_eSPI& tft, int16_t x, int16_t y, int16_t w, int16_t h, +void drawSlideBar(lgfx::LGFX_Device& tft, int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color, uint16_t trackColor) { // Draw track (also erases previous segment position) tft.fillRoundRect(x, y, w, h, h / 2, trackColor); @@ -95,7 +95,7 @@ static unsigned long completionStart = 0; static bool completionDone = false; static int16_t prevRing = 0; -void drawCompletionAnim(TFT_eSPI& tft, int16_t cx, int16_t cy, bool reset) { +void drawCompletionAnim(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, bool reset) { if (reset) { completionStart = millis(); completionDone = false; @@ -113,18 +113,18 @@ void drawCompletionAnim(TFT_eSPI& tft, int16_t cx, int16_t cy, bool reset) { int16_t r = 10 + (elapsed * (finalR - 10)) / 400; // Erase previous ring if (prevRing > 0 && prevRing != r) { - tft.drawSmoothArc(cx, cy, prevRing, prevRing - 3, 0, 360, CLR_BG, CLR_BG, false); + tft.drawArc(cx, cy, prevRing, prevRing - 3, 0, 360, CLR_BG); } - tft.drawSmoothArc(cx, cy, r, r - 3, 0, 360, CLR_GREEN, CLR_BG, false); + tft.drawArc(cx, cy, r, r - 3, 0, 360, CLR_GREEN); prevRing = r; } // Phase 2 (400-600ms): settle to final ring else if (elapsed < 600) { - tft.drawSmoothArc(cx, cy, finalR, finalR - 4, 0, 360, CLR_GREEN, CLR_BG, false); + tft.drawArc(cx, cy, finalR, finalR - 4, 0, 360, CLR_GREEN); } // Phase 3 (600ms+): static ring + large checkmark, done else { - tft.drawSmoothArc(cx, cy, finalR, finalR - 4, 0, 360, CLR_GREEN, CLR_BG, false); + tft.drawArc(cx, cy, finalR, finalR - 4, 0, 360, CLR_GREEN); // Clear center for checkmark tft.fillCircle(cx, cy, finalR - 5, CLR_BG); // Draw 32x32 checkmark centered diff --git a/src/display_anim.h b/src/display_anim.h index ee45cea..aa585c1 100644 --- a/src/display_anim.h +++ b/src/display_anim.h @@ -1,23 +1,23 @@ #ifndef DISPLAY_ANIM_H #define DISPLAY_ANIM_H -#include +#include // Rotating arc spinner for connecting screens -void drawSpinner(TFT_eSPI& tft, int16_t cx, int16_t cy, int16_t radius, +void drawSpinner(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, int16_t radius, uint16_t color); // Animated dots "..." that cycle (call each frame, uses millis()) -void drawAnimDots(TFT_eSPI& tft, int16_t x, int16_t y, uint16_t color); +void drawAnimDots(lgfx::LGFX_Device& tft, int16_t x, int16_t y, uint16_t color); // Pulsing glow on arc edge (returns brightness factor 0.5-1.0) float getPulseFactor(); // Indeterminate slide bar (call each frame — uses millis() internally) -void drawSlideBar(TFT_eSPI& tft, int16_t x, int16_t y, int16_t w, int16_t h, +void drawSlideBar(lgfx::LGFX_Device& tft, int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color, uint16_t trackColor); // Completion animation: expanding checkmark ring -void drawCompletionAnim(TFT_eSPI& tft, int16_t cx, int16_t cy, bool reset); +void drawCompletionAnim(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, bool reset); #endif // DISPLAY_ANIM_H diff --git a/src/display_gauges.cpp b/src/display_gauges.cpp index f223415..3d6c34e 100644 --- a/src/display_gauges.cpp +++ b/src/display_gauges.cpp @@ -3,10 +3,19 @@ #include "layout.h" #include "settings.h" +// LovyanGFX does not expose alphaBlend() as a member. Provide a compatible +// helper: alpha=0 → pure bg, alpha=255 → pure fg (same semantics as TFT_eSPI). +static inline uint16_t alphaBlend565(uint8_t alpha, uint16_t fg, uint16_t bg) { + uint8_t r = ((fg >> 11) & 0x1F) * alpha / 255 + ((bg >> 11) & 0x1F) * (255 - alpha) / 255; + uint8_t g = ((fg >> 5) & 0x3F) * alpha / 255 + ((bg >> 5) & 0x3F) * (255 - alpha) / 255; + uint8_t b = ( fg & 0x1F) * alpha / 255 + ( bg & 0x1F) * (255 - alpha) / 255; + return (r << 11) | (g << 5) | b; +} + // --------------------------------------------------------------------------- // H2-style LED progress bar // --------------------------------------------------------------------------- -void drawLedProgressBar(TFT_eSPI& tft, int16_t y, uint8_t progress) { +void drawLedProgressBar(lgfx::LGFX_Device& tft, int16_t y, uint8_t progress) { uint16_t bg = dispSettings.bgColor; uint16_t track = dispSettings.trackColor; @@ -25,7 +34,7 @@ void drawLedProgressBar(TFT_eSPI& tft, int16_t y, uint8_t progress) { tft.fillRoundRect(barX, y, fillW, barH, 2, barColor); - uint16_t glowColor = tft.alphaBlend(160, CLR_TEXT, barColor); + uint16_t glowColor = alphaBlend565(160, CLR_TEXT, barColor); tft.drawFastHLine(barX + 1, y + barH / 2, fillW - 2, glowColor); if (fillW > 4 && progress < 100) { @@ -50,7 +59,7 @@ static const uint16_t SHIMMER_INTERVAL = 25; // ms between steps (~40fps) static const uint16_t SHIMMER_PAUSE = 1200; // ms pause between sweeps static const int16_t SHIMMER_STEP = 3; // pixels per step -void tickProgressShimmer(TFT_eSPI& tft, int16_t y, uint8_t progress, bool printing) { +void tickProgressShimmer(lgfx::LGFX_Device& tft, int16_t y, uint8_t progress, bool printing) { if (!dispSettings.animatedBar || !printing || progress == 0) return; unsigned long now = millis(); @@ -89,8 +98,8 @@ void tickProgressShimmer(TFT_eSPI& tft, int16_t y, uint8_t progress, bool printi if (sx + sw > barX + fillW) sw = barX + fillW - sx; if (sw > 0) { // Gradient-like shimmer: brighter in center - uint16_t bright = tft.alphaBlend(180, CLR_TEXT, barColor); - uint16_t mid = tft.alphaBlend(100, CLR_TEXT, barColor); + uint16_t bright = alphaBlend565(180, CLR_TEXT, barColor); + uint16_t mid = alphaBlend565(100, CLR_TEXT, barColor); // Edge pixels if (sw >= 3) { tft.fillRect(sx, y, 2, barH, mid); @@ -110,7 +119,7 @@ void tickProgressShimmer(TFT_eSPI& tft, int16_t y, uint8_t progress, bool printi if (tailX < barX) tailX = barX; tft.fillRect(tailX, y, barX + fillW - tailX, barH, barColor); // Re-draw center glow line - uint16_t glowColor = tft.alphaBlend(160, CLR_TEXT, barColor); + uint16_t glowColor = alphaBlend565(160, CLR_TEXT, barColor); tft.drawFastHLine(barX + 1, y + barH / 2, fillW - 2, glowColor); shimmerPos = 0; @@ -122,37 +131,50 @@ void tickProgressShimmer(TFT_eSPI& tft, int16_t y, uint8_t progress, bool printi // --------------------------------------------------------------------------- // Helper: draw arc track + fill, handling decrease properly // --------------------------------------------------------------------------- -static void drawArcFill(TFT_eSPI& tft, int16_t cx, int16_t cy, +static void drawArcFill(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, int16_t radius, int16_t thickness, uint16_t fillEnd, uint16_t fillColor, bool forceRedraw) { + // Internal angles use TFT_eSPI convention: 0°=bottom (6 o'clock), clockwise. + // LovyanGFX fillArc uses 0°=right (3 o'clock), clockwise — offset by +90° + // places the 120° gap at the bottom (6 o'clock), matching the desired layout. + // When the converted start > end the arc crosses 0°, so split into two calls. const uint16_t startAngle = 60; const uint16_t endAngle = 300; uint16_t bg = dispSettings.bgColor; uint16_t track = dispSettings.trackColor; + auto arcDraw = [&](uint16_t a0, uint16_t a1, uint16_t color) { + float la0 = (float)((a0 + 90u) % 360u); + float la1 = (float)((a1 + 90u) % 360u); + if (la0 > la1) { + // Arc crosses the 0° boundary — split into two segments + tft.fillArc(cx, cy, radius, radius - thickness, la0, 360.0f, color); + tft.fillArc(cx, cy, radius, radius - thickness, 0.0f, la1, color); + } else { + tft.fillArc(cx, cy, radius, radius - thickness, la0, la1, color); + } + }; + if (forceRedraw) { tft.fillCircle(cx, cy, radius + 2, bg); - tft.drawSmoothArc(cx, cy, radius, radius - thickness, - startAngle, endAngle, track, bg, false); + arcDraw(startAngle, endAngle, track); } // Draw filled portion if (fillEnd > startAngle) { - tft.drawSmoothArc(cx, cy, radius, radius - thickness, - startAngle, fillEnd, fillColor, bg, false); + arcDraw(startAngle, fillEnd, fillColor); } // Always redraw track for unfilled portion (handles value decrease) if (fillEnd < endAngle) { - tft.drawSmoothArc(cx, cy, radius, radius - thickness, - fillEnd, endAngle, track, bg, false); + arcDraw(fillEnd, endAngle, track); } } // --------------------------------------------------------------------------- // Helper: clear gauge center and prepare for text // --------------------------------------------------------------------------- -static void clearGaugeCenter(TFT_eSPI& tft, int16_t cx, int16_t cy, +static void clearGaugeCenter(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, int16_t radius, int16_t thickness) { int16_t textR = radius - thickness - 1; tft.fillCircle(cx, cy, textR, dispSettings.bgColor); @@ -217,7 +239,7 @@ void resetGaugeTextCache() { // --------------------------------------------------------------------------- // Main progress arc // --------------------------------------------------------------------------- -void drawProgressArc(TFT_eSPI& tft, int16_t cx, int16_t cy, int16_t radius, +void drawProgressArc(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, int16_t radius, int16_t thickness, uint8_t progress, uint8_t prevProgress, uint16_t remainingMin, bool forceRedraw) { const uint16_t startAngle = 60; @@ -270,7 +292,7 @@ void drawProgressArc(TFT_eSPI& tft, int16_t cx, int16_t cy, int16_t radius, // --------------------------------------------------------------------------- // Temperature arc gauge // --------------------------------------------------------------------------- -void drawTempGauge(TFT_eSPI& tft, int16_t cx, int16_t cy, int16_t radius, +void drawTempGauge(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, int16_t radius, float current, float target, float maxTemp, uint16_t accentColor, const char* label, const uint8_t* icon, bool forceRedraw, @@ -328,7 +350,7 @@ void drawTempGauge(TFT_eSPI& tft, int16_t cx, int16_t cy, int16_t radius, // --------------------------------------------------------------------------- // Fan speed gauge (0-100%) // --------------------------------------------------------------------------- -void drawFanGauge(TFT_eSPI& tft, int16_t cx, int16_t cy, int16_t radius, +void drawFanGauge(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, int16_t radius, uint8_t percent, uint16_t accentColor, const char* label, bool forceRedraw, const GaugeColors* colors, float arcPercent) { diff --git a/src/display_gauges.h b/src/display_gauges.h index 3efd41b..066f1a3 100644 --- a/src/display_gauges.h +++ b/src/display_gauges.h @@ -1,24 +1,24 @@ #ifndef DISPLAY_GAUGES_H #define DISPLAY_GAUGES_H -#include +#include struct GaugeColors; // forward declaration from settings.h // Draw H2-style LED progress bar (full-width, top of screen) -void drawLedProgressBar(TFT_eSPI& tft, int16_t y, uint8_t progress); +void drawLedProgressBar(lgfx::LGFX_Device& tft, int16_t y, uint8_t progress); // Shimmer animation tick — call from loop(), runs at its own cadence -void tickProgressShimmer(TFT_eSPI& tft, int16_t y, uint8_t progress, bool printing); +void tickProgressShimmer(lgfx::LGFX_Device& tft, int16_t y, uint8_t progress, bool printing); // Draw progress arc with percentage and time in center -void drawProgressArc(TFT_eSPI& tft, int16_t cx, int16_t cy, int16_t radius, +void drawProgressArc(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, int16_t radius, int16_t thickness, uint8_t progress, uint8_t prevProgress, uint16_t remainingMin, bool forceRedraw); // Draw temperature arc gauge with current/target // arcValue: smooth value for arc position, current: actual value for text display -void drawTempGauge(TFT_eSPI& tft, int16_t cx, int16_t cy, int16_t radius, +void drawTempGauge(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, int16_t radius, float current, float target, float maxTemp, uint16_t accentColor, const char* label, const uint8_t* icon, bool forceRedraw, @@ -27,7 +27,7 @@ void drawTempGauge(TFT_eSPI& tft, int16_t cx, int16_t cy, int16_t radius, // Draw fan speed gauge (0-100%) // arcPercent: smooth value for arc position (-1 = use percent) -void drawFanGauge(TFT_eSPI& tft, int16_t cx, int16_t cy, int16_t radius, +void drawFanGauge(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, int16_t radius, uint8_t percent, uint16_t accentColor, const char* label, bool forceRedraw, const GaugeColors* colors = nullptr, float arcPercent = -1.0f); diff --git a/src/display_ui.cpp b/src/display_ui.cpp index a4ad76b..cb9972c 100644 --- a/src/display_ui.cpp +++ b/src/display_ui.cpp @@ -13,7 +13,138 @@ #include #include -TFT_eSPI tft = TFT_eSPI(); +// ============================================================================= +// LovyanGFX board-specific configurations +// ============================================================================= + +#if defined(BOARD_IS_S3) +// --- ESP32-S3 Super Mini + ST7789 240x240 ------------------------------------ +class LGFX_S3 : public lgfx::LGFX_Device { + lgfx::Panel_ST7789 _panel; + lgfx::Bus_SPI _bus; +public: + LGFX_S3() { + { + auto cfg = _bus.config(); + cfg.spi_host = SPI2_HOST; + cfg.spi_mode = 0; + cfg.freq_write = 40000000; + cfg.freq_read = 16000000; + cfg.pin_sclk = 12; + cfg.pin_mosi = 11; + cfg.pin_miso = -1; + cfg.pin_dc = 9; + cfg.use_lock = true; + _bus.config(cfg); + _panel.setBus(&_bus); + } + { + auto cfg = _panel.config(); + cfg.pin_cs = 10; + cfg.pin_rst = 8; + cfg.pin_busy = -1; + cfg.memory_width = 240; + cfg.memory_height = 240; + cfg.panel_width = 240; + cfg.panel_height = 240; + cfg.offset_x = 0; + cfg.offset_y = 0; + cfg.readable = false; + _panel.config(cfg); + } + setPanel(&_panel); + } +}; +static LGFX_S3 _tft_instance; + +#elif defined(DISPLAY_CYD) +// --- ESP32-2432S028 (CYD) + ILI9342 240x320 --------------------------------- +class LGFX_CYD : public lgfx::LGFX_Device { + lgfx::Panel_ILI9342 _panel; + lgfx::Bus_SPI _bus; +public: + LGFX_CYD() { + { + auto cfg = _bus.config(); + cfg.spi_host = VSPI_HOST; + cfg.spi_mode = 0; + cfg.freq_write = 27000000; + cfg.freq_read = 16000000; + cfg.pin_sclk = 14; + cfg.pin_mosi = 13; + cfg.pin_miso = -1; + cfg.pin_dc = 2; + cfg.use_lock = true; + _bus.config(cfg); + _panel.setBus(&_bus); + } + { + auto cfg = _panel.config(); + cfg.pin_cs = 15; + cfg.pin_rst = 12; + cfg.pin_busy = -1; + cfg.memory_width = 240; + cfg.memory_height = 320; + cfg.panel_width = 240; + cfg.panel_height = 320; + cfg.offset_x = 0; + cfg.offset_y = 0; + cfg.invert = true; + cfg.readable = false; + _panel.config(cfg); + } + setPanel(&_panel); + } +}; +static LGFX_CYD _tft_instance; + +#elif defined(BOARD_IS_C3) +// --- ESP32-C3 Super Mini + ST7789 240x280 ------------------------------------ +class LGFX_C3 : public lgfx::LGFX_Device { + lgfx::Panel_ST7789 _panel; + lgfx::Bus_SPI _bus; +public: + LGFX_C3() { + { + auto cfg = _bus.config(); + cfg.spi_host = SPI2_HOST; + cfg.spi_mode = 0; + cfg.freq_write = 40000000; + cfg.freq_read = 16000000; + cfg.pin_sclk = 21; + cfg.pin_mosi = 20; + cfg.pin_miso = -1; + cfg.pin_dc = 7; + cfg.use_lock = true; + _bus.config(cfg); + _panel.setBus(&_bus); + } + { + auto cfg = _panel.config(); + cfg.pin_cs = 6; + cfg.pin_rst = 10; + cfg.pin_busy = -1; + cfg.memory_width = 240; + cfg.memory_height = 280; + cfg.panel_width = 240; + cfg.panel_height = 280; + cfg.offset_x = 0; + cfg.offset_y = 0; + cfg.readable = false; + _panel.config(cfg); + } + setPanel(&_panel); + } +}; +static LGFX_C3 _tft_instance; + +#else + #error "No board variant defined. Add BOARD_IS_S3, DISPLAY_CYD, or BOARD_IS_C3 to build_flags." +#endif + +// Global pointer + reference — accessed via `tft` throughout the codebase +lgfx::LGFX_Device* tft_ptr = &_tft_instance; +lgfx::LGFX_Device& tft = *tft_ptr; // Use user-configured bg color instead of hardcoded CLR_BG #undef CLR_BG @@ -98,7 +229,10 @@ void initDisplay() { Serial.println("Display: pre-init delay..."); delay(500); Serial.println("Display: calling tft.init()..."); - tft.init(); // TFT_eSPI configures SPI from build flags + tft.init(); // LovyanGFX configures SPI from the board class above +#if defined(BOARD_IS_S3) + tft.invertDisplay(true); // ST7789 on S3 Super Mini requires color inversion +#endif Serial.println("Display: tft.init() done"); #if defined(DISPLAY_CYD) // Clear entire GRAM at rotation 0 first (guarantees all 240x320 pixels @@ -113,8 +247,9 @@ void initDisplay() { Serial.println("Display: fillScreen done"); #if defined(TOUCH_CS) && !defined(USE_XPT2046) - uint16_t calData[5] = {321, 3498, 280, 3584, 3}; - tft.setTouch(calData); + // LovyanGFX touch calibration + uint16_t calData[8] = {0, 0, 0, 65535, 0, 65535, 65535, 65535}; + tft.setTouchCalibrate(calData); Serial.println("Display: touch calibration set"); #endif @@ -295,7 +430,6 @@ static void drawWiFiConnected() { // --------------------------------------------------------------------------- // Screen: OTA firmware update in progress // --------------------------------------------------------------------------- -#ifdef ENABLE_OTA_AUTO #include "web_server.h" static void drawOtaUpdate() { tft.setTextDatum(MC_DATUM); @@ -334,7 +468,6 @@ static void drawOtaUpdate() { tft.setTextColor(CLR_ORANGE, CLR_BG); tft.drawString("Do not power off", SCREEN_W / 2, SCREEN_H / 2 + 58); } -#endif // ENABLE_OTA_AUTO // --------------------------------------------------------------------------- // Screen: Connecting MQTT @@ -1429,11 +1562,9 @@ void updateDisplay() { drawConnectingMQTT(); break; -#ifdef ENABLE_OTA_AUTO case SCREEN_OTA_UPDATE: drawOtaUpdate(); break; -#endif case SCREEN_IDLE: drawIdle(); diff --git a/src/display_ui.h b/src/display_ui.h index 533cf7f..a9a78f6 100644 --- a/src/display_ui.h +++ b/src/display_ui.h @@ -1,7 +1,7 @@ #ifndef DISPLAY_UI_H #define DISPLAY_UI_H -#include +#include enum ScreenState { SCREEN_SPLASH, @@ -17,7 +17,9 @@ enum ScreenState { SCREEN_OTA_UPDATE }; -extern TFT_eSPI tft; +extern lgfx::LGFX_Device* tft_ptr; +// Convenience reference — all callers use `tft.method()` unchanged +extern lgfx::LGFX_Device& tft; void initDisplay(); void updateDisplay(); diff --git a/src/icons.h b/src/icons.h index 05c8a2f..38f02d1 100644 --- a/src/icons.h +++ b/src/icons.h @@ -2,6 +2,7 @@ #define ICONS_H #include +#include // 16x16 1-bit icons stored as PROGMEM byte arrays (1 bit per pixel). // Draw with: for each bit, if set draw accentColor, else skip (transparent). @@ -264,7 +265,7 @@ const uint8_t PROGMEM icon_lightning[] = { }; // Helper: draw a 16x16 1-bit icon at (x, y) with given color, transparent bg -inline void drawIcon16(TFT_eSPI& tft, int16_t x, int16_t y, +inline void drawIcon16(lgfx::LGFX_Device& tft, int16_t x, int16_t y, const uint8_t* icon, uint16_t color) { for (int row = 0; row < 16; row++) { uint8_t b0 = pgm_read_byte(&icon[row * 2]); @@ -279,7 +280,7 @@ inline void drawIcon16(TFT_eSPI& tft, int16_t x, int16_t y, } // Helper: draw a 32x32 1-bit icon at (x, y) with given color, transparent bg -inline void drawIcon32(TFT_eSPI& tft, int16_t x, int16_t y, +inline void drawIcon32(lgfx::LGFX_Device& tft, int16_t x, int16_t y, const uint8_t* icon, uint16_t color) { for (int row = 0; row < 32; row++) { uint32_t bits = ((uint32_t)pgm_read_byte(&icon[row * 4]) << 24) | From f933f044271fcb085ed6c6c1b40e94121a5deef1 Mon Sep 17 00:00:00 2001 From: Niels Timmer Date: Tue, 31 Mar 2026 20:34:28 +0200 Subject: [PATCH 4/6] Add esp32c3-headless build env with HTTP screenshot endpoint New [env:esp32c3-headless] skips physical display init (tft.init, backlight) and instead allocates a 240x280 LGFX_Sprite as a RAM framebuffer. The full rendering pipeline runs unchanged against the sprite. Adds GET /screenshot (serves sprite as BMP) and GET /display (HTML page with 2s auto-refresh) so display output can be observed in any browser without a physical screen attached. Useful for debugging display logic on a bare C3 module. --- platformio.ini | 18 +++++++++++++ src/display_ui.cpp | 16 ++++++++++-- src/web_server.cpp | 63 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 2 deletions(-) diff --git a/platformio.ini b/platformio.ini index 010bcab..25aad2b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -71,3 +71,21 @@ build_flags = -D BACKLIGHT_PIN=5 ; --- USB Serial (C3 native USB CDC) --- -D ARDUINO_USB_CDC_ON_BOOT=1 + +; ============================================================================= +; ESP32-C3 Super Mini — headless (no physical screen, virtual display via HTTP) +; ============================================================================= +[env:esp32c3-headless] +platform = espressif32 +board = lolin_c3_mini +framework = arduino +monitor_speed = 115200 +board_build.partitions = partitions_4mb_c3.csv +lib_deps = ${common.lib_deps} +build_flags = + -D BOARD_VARIANT=\"esp32c3\" + -D BOARD_IS_C3=1 + -D ENABLE_OTA_AUTO=1 + -D BACKLIGHT_PIN=5 + -D ARDUINO_USB_CDC_ON_BOOT=1 + -D HEADLESS=1 diff --git a/src/display_ui.cpp b/src/display_ui.cpp index cb9972c..612a5be 100644 --- a/src/display_ui.cpp +++ b/src/display_ui.cpp @@ -143,6 +143,10 @@ static LGFX_C3 _tft_instance; #endif // Global pointer + reference — accessed via `tft` throughout the codebase +#ifdef HEADLESS +lgfx::LGFX_Sprite headlessSprite; +bool headlessSpriteReady = false; +#endif lgfx::LGFX_Device* tft_ptr = &_tft_instance; lgfx::LGFX_Device& tft = *tft_ptr; @@ -216,7 +220,7 @@ static bool tickGaugeSmooth(const BambuState& s, bool snap) { static uint8_t lastAppliedBrightness = 0; void setBacklight(uint8_t level) { -#if defined(BACKLIGHT_PIN) && BACKLIGHT_PIN >= 0 +#if defined(BACKLIGHT_PIN) && BACKLIGHT_PIN >= 0 && !defined(HEADLESS) analogWrite(BACKLIGHT_PIN, level); #endif lastAppliedBrightness = level; @@ -228,12 +232,20 @@ void setBacklight(uint8_t level) { void initDisplay() { Serial.println("Display: pre-init delay..."); delay(500); +#ifndef HEADLESS Serial.println("Display: calling tft.init()..."); tft.init(); // LovyanGFX configures SPI from the board class above #if defined(BOARD_IS_S3) tft.invertDisplay(true); // ST7789 on S3 Super Mini requires color inversion #endif Serial.println("Display: tft.init() done"); +#endif // HEADLESS +#ifdef HEADLESS + headlessSprite.setColorDepth(16); + headlessSprite.createSprite(240, 280); + headlessSpriteReady = true; + Serial.println("HEADLESS mode: sprite framebuffer ready (240x280)"); +#endif #if defined(DISPLAY_CYD) // Clear entire GRAM at rotation 0 first (guarantees all 240x320 pixels // are addressed). Without this, rotations 1/3 leave 80px of uninitialized @@ -253,7 +265,7 @@ void initDisplay() { Serial.println("Display: touch calibration set"); #endif -#if defined(BACKLIGHT_PIN) && BACKLIGHT_PIN >= 0 +#if defined(BACKLIGHT_PIN) && BACKLIGHT_PIN >= 0 && !defined(HEADLESS) pinMode(BACKLIGHT_PIN, OUTPUT); setBacklight(200); #endif diff --git a/src/web_server.cpp b/src/web_server.cpp index be94f12..3d40d72 100644 --- a/src/web_server.cpp +++ b/src/web_server.cpp @@ -2365,6 +2365,69 @@ void initWebServer() { #ifdef ENABLE_OTA_AUTO server.on("/ota/auto", HTTP_POST, handleOtaAuto); server.on("/ota/status", HTTP_GET, handleOtaStatus); +#endif +#ifdef HEADLESS + server.on("/screenshot", HTTP_GET, []() { + extern lgfx::LGFX_Sprite headlessSprite; + extern bool headlessSpriteReady; + if (!headlessSpriteReady) { + server.send(503, "text/plain", "Sprite not ready"); + return; + } + int w = headlessSprite.width(); + int h = headlessSprite.height(); + // BMP header (54 bytes): BITMAPFILEHEADER + BITMAPINFOHEADER + // RGB565 pixels stored as BGR24 in BMP + int rowSize = ((w * 3 + 3) & ~3); // padded to 4-byte boundary + int imageSize = rowSize * h; + int fileSize = 54 + imageSize; + uint8_t* bmp = (uint8_t*)malloc(fileSize); + if (!bmp) { + server.send(500, "text/plain", "OOM"); + return; + } + memset(bmp, 0, 54); + // BITMAPFILEHEADER + bmp[0] = 'B'; bmp[1] = 'M'; + bmp[2] = fileSize & 0xFF; bmp[3] = (fileSize >> 8) & 0xFF; + bmp[4] = (fileSize >> 16) & 0xFF; bmp[5] = (fileSize >> 24) & 0xFF; + bmp[10] = 54; // pixel data offset + // BITMAPINFOHEADER + bmp[14] = 40; // header size + bmp[18] = w & 0xFF; bmp[19] = (w >> 8) & 0xFF; + // height negative = top-down + int negH = -h; + bmp[22] = negH & 0xFF; bmp[23] = (negH >> 8) & 0xFF; + bmp[24] = (negH >> 16) & 0xFF; bmp[25] = (negH >> 24) & 0xFF; + bmp[26] = 1; // planes + bmp[28] = 24; // bits per pixel (BGR24) + bmp[34] = imageSize & 0xFF; bmp[35] = (imageSize >> 8) & 0xFF; + bmp[36] = (imageSize >> 16) & 0xFF; bmp[37] = (imageSize >> 24) & 0xFF; + // Pixel data: BGR24, top-down (negative height) + uint8_t* px = bmp + 54; + for (int y = 0; y < h; y++) { + uint8_t* row = px + y * rowSize; + for (int x = 0; x < w; x++) { + uint16_t c = headlessSprite.readPixel(x, y); + row[x*3+0] = (c & 0x001F) << 3; // B + row[x*3+1] = ((c >> 5) & 0x3F) << 2; // G + row[x*3+2] = ((c >> 11) & 0x1F) << 3; // R + } + } + server.setContentLength(fileSize); + server.send(200, "image/bmp", ""); + server.sendContent((const char*)bmp, fileSize); + free(bmp); + }); + + server.on("/display", HTTP_GET, []() { + server.send(200, "text/html", + "BambuHelper Display" + "" + "" + ""); + }); #endif server.onNotFound(handleNotFound); server.begin(); From 781218c68e596debffc91dc2f056589625dc087d Mon Sep 17 00:00:00 2001 From: Niels Timmer Date: Tue, 31 Mar 2026 21:05:04 +0200 Subject: [PATCH 5/6] Fix headless display: redirect tft to sprite, stream BMP to avoid OOM - Change tft type from LGFX_Device to LovyanGFX (common base) so it can point to either hardware display or LGFX_Sprite in headless mode - In HEADLESS mode, tft now targets the sprite framebuffer so all draw calls are captured for the /screenshot endpoint - Stream BMP row-by-row instead of malloc'ing the full 197KB image, fixing OOM crash on ESP32-C3 (no PSRAM) - Update all helper functions (gauges, anims, icons) to accept LovyanGFX& instead of LGFX_Device& --- src/display_anim.cpp | 8 +++--- src/display_anim.h | 8 +++--- src/display_gauges.cpp | 14 +++++----- src/display_gauges.h | 10 +++---- src/display_ui.cpp | 10 ++++--- src/display_ui.h | 4 +-- src/icons.h | 4 +-- src/web_server.cpp | 59 +++++++++++++++++++----------------------- 8 files changed, 56 insertions(+), 61 deletions(-) diff --git a/src/display_anim.cpp b/src/display_anim.cpp index 7ab12ee..a41fb1a 100644 --- a/src/display_anim.cpp +++ b/src/display_anim.cpp @@ -12,7 +12,7 @@ // --------------------------------------------------------------------------- static uint16_t spinnerAngle = 0; -void drawSpinner(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, int16_t radius, +void drawSpinner(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, uint16_t color) { // Erase previous arc segment (handle wrap-around) uint16_t prevStart = (spinnerAngle + 360 - 12) % 360; @@ -47,7 +47,7 @@ void drawSpinner(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, int16_t radius, // --------------------------------------------------------------------------- // Animated dots "..." // --------------------------------------------------------------------------- -void drawAnimDots(lgfx::LGFX_Device& tft, int16_t x, int16_t y, uint16_t color) { +void drawAnimDots(lgfx::LovyanGFX& tft, int16_t x, int16_t y, uint16_t color) { unsigned long ms = millis(); int phase = (ms / 400) % 4; @@ -64,7 +64,7 @@ void drawAnimDots(lgfx::LGFX_Device& tft, int16_t x, int16_t y, uint16_t color) // --------------------------------------------------------------------------- // Indeterminate slide bar — a glowing segment slides back and forth // --------------------------------------------------------------------------- -void drawSlideBar(lgfx::LGFX_Device& tft, int16_t x, int16_t y, int16_t w, int16_t h, +void drawSlideBar(lgfx::LovyanGFX& tft, int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color, uint16_t trackColor) { // Draw track (also erases previous segment position) tft.fillRoundRect(x, y, w, h, h / 2, trackColor); @@ -95,7 +95,7 @@ static unsigned long completionStart = 0; static bool completionDone = false; static int16_t prevRing = 0; -void drawCompletionAnim(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, bool reset) { +void drawCompletionAnim(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, bool reset) { if (reset) { completionStart = millis(); completionDone = false; diff --git a/src/display_anim.h b/src/display_anim.h index aa585c1..64a72ce 100644 --- a/src/display_anim.h +++ b/src/display_anim.h @@ -4,20 +4,20 @@ #include // Rotating arc spinner for connecting screens -void drawSpinner(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, int16_t radius, +void drawSpinner(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, uint16_t color); // Animated dots "..." that cycle (call each frame, uses millis()) -void drawAnimDots(lgfx::LGFX_Device& tft, int16_t x, int16_t y, uint16_t color); +void drawAnimDots(lgfx::LovyanGFX& tft, int16_t x, int16_t y, uint16_t color); // Pulsing glow on arc edge (returns brightness factor 0.5-1.0) float getPulseFactor(); // Indeterminate slide bar (call each frame — uses millis() internally) -void drawSlideBar(lgfx::LGFX_Device& tft, int16_t x, int16_t y, int16_t w, int16_t h, +void drawSlideBar(lgfx::LovyanGFX& tft, int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color, uint16_t trackColor); // Completion animation: expanding checkmark ring -void drawCompletionAnim(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, bool reset); +void drawCompletionAnim(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, bool reset); #endif // DISPLAY_ANIM_H diff --git a/src/display_gauges.cpp b/src/display_gauges.cpp index 3d6c34e..8461459 100644 --- a/src/display_gauges.cpp +++ b/src/display_gauges.cpp @@ -15,7 +15,7 @@ static inline uint16_t alphaBlend565(uint8_t alpha, uint16_t fg, uint16_t bg) { // --------------------------------------------------------------------------- // H2-style LED progress bar // --------------------------------------------------------------------------- -void drawLedProgressBar(lgfx::LGFX_Device& tft, int16_t y, uint8_t progress) { +void drawLedProgressBar(lgfx::LovyanGFX& tft, int16_t y, uint8_t progress) { uint16_t bg = dispSettings.bgColor; uint16_t track = dispSettings.trackColor; @@ -59,7 +59,7 @@ static const uint16_t SHIMMER_INTERVAL = 25; // ms between steps (~40fps) static const uint16_t SHIMMER_PAUSE = 1200; // ms pause between sweeps static const int16_t SHIMMER_STEP = 3; // pixels per step -void tickProgressShimmer(lgfx::LGFX_Device& tft, int16_t y, uint8_t progress, bool printing) { +void tickProgressShimmer(lgfx::LovyanGFX& tft, int16_t y, uint8_t progress, bool printing) { if (!dispSettings.animatedBar || !printing || progress == 0) return; unsigned long now = millis(); @@ -131,7 +131,7 @@ void tickProgressShimmer(lgfx::LGFX_Device& tft, int16_t y, uint8_t progress, bo // --------------------------------------------------------------------------- // Helper: draw arc track + fill, handling decrease properly // --------------------------------------------------------------------------- -static void drawArcFill(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, +static void drawArcFill(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, int16_t thickness, uint16_t fillEnd, uint16_t fillColor, bool forceRedraw) { // Internal angles use TFT_eSPI convention: 0°=bottom (6 o'clock), clockwise. @@ -174,7 +174,7 @@ static void drawArcFill(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, // --------------------------------------------------------------------------- // Helper: clear gauge center and prepare for text // --------------------------------------------------------------------------- -static void clearGaugeCenter(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, +static void clearGaugeCenter(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, int16_t thickness) { int16_t textR = radius - thickness - 1; tft.fillCircle(cx, cy, textR, dispSettings.bgColor); @@ -239,7 +239,7 @@ void resetGaugeTextCache() { // --------------------------------------------------------------------------- // Main progress arc // --------------------------------------------------------------------------- -void drawProgressArc(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, int16_t radius, +void drawProgressArc(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, int16_t thickness, uint8_t progress, uint8_t prevProgress, uint16_t remainingMin, bool forceRedraw) { const uint16_t startAngle = 60; @@ -292,7 +292,7 @@ void drawProgressArc(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, int16_t rad // --------------------------------------------------------------------------- // Temperature arc gauge // --------------------------------------------------------------------------- -void drawTempGauge(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, int16_t radius, +void drawTempGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, float current, float target, float maxTemp, uint16_t accentColor, const char* label, const uint8_t* icon, bool forceRedraw, @@ -350,7 +350,7 @@ void drawTempGauge(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, int16_t radiu // --------------------------------------------------------------------------- // Fan speed gauge (0-100%) // --------------------------------------------------------------------------- -void drawFanGauge(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, int16_t radius, +void drawFanGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, uint8_t percent, uint16_t accentColor, const char* label, bool forceRedraw, const GaugeColors* colors, float arcPercent) { diff --git a/src/display_gauges.h b/src/display_gauges.h index 066f1a3..53e5263 100644 --- a/src/display_gauges.h +++ b/src/display_gauges.h @@ -6,19 +6,19 @@ struct GaugeColors; // forward declaration from settings.h // Draw H2-style LED progress bar (full-width, top of screen) -void drawLedProgressBar(lgfx::LGFX_Device& tft, int16_t y, uint8_t progress); +void drawLedProgressBar(lgfx::LovyanGFX& tft, int16_t y, uint8_t progress); // Shimmer animation tick — call from loop(), runs at its own cadence -void tickProgressShimmer(lgfx::LGFX_Device& tft, int16_t y, uint8_t progress, bool printing); +void tickProgressShimmer(lgfx::LovyanGFX& tft, int16_t y, uint8_t progress, bool printing); // Draw progress arc with percentage and time in center -void drawProgressArc(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, int16_t radius, +void drawProgressArc(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, int16_t thickness, uint8_t progress, uint8_t prevProgress, uint16_t remainingMin, bool forceRedraw); // Draw temperature arc gauge with current/target // arcValue: smooth value for arc position, current: actual value for text display -void drawTempGauge(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, int16_t radius, +void drawTempGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, float current, float target, float maxTemp, uint16_t accentColor, const char* label, const uint8_t* icon, bool forceRedraw, @@ -27,7 +27,7 @@ void drawTempGauge(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, int16_t radiu // Draw fan speed gauge (0-100%) // arcPercent: smooth value for arc position (-1 = use percent) -void drawFanGauge(lgfx::LGFX_Device& tft, int16_t cx, int16_t cy, int16_t radius, +void drawFanGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, uint8_t percent, uint16_t accentColor, const char* label, bool forceRedraw, const GaugeColors* colors = nullptr, float arcPercent = -1.0f); diff --git a/src/display_ui.cpp b/src/display_ui.cpp index 612a5be..a3742ca 100644 --- a/src/display_ui.cpp +++ b/src/display_ui.cpp @@ -146,9 +146,11 @@ static LGFX_C3 _tft_instance; #ifdef HEADLESS lgfx::LGFX_Sprite headlessSprite; bool headlessSpriteReady = false; +lgfx::LovyanGFX* tft_ptr = &headlessSprite; // draw into sprite, not hardware +#else +lgfx::LovyanGFX* tft_ptr = &_tft_instance; #endif -lgfx::LGFX_Device* tft_ptr = &_tft_instance; -lgfx::LGFX_Device& tft = *tft_ptr; +lgfx::LovyanGFX& tft = *tft_ptr; // Use user-configured bg color instead of hardcoded CLR_BG #undef CLR_BG @@ -234,9 +236,9 @@ void initDisplay() { delay(500); #ifndef HEADLESS Serial.println("Display: calling tft.init()..."); - tft.init(); // LovyanGFX configures SPI from the board class above + _tft_instance.init(); // LovyanGFX configures SPI from the board class above #if defined(BOARD_IS_S3) - tft.invertDisplay(true); // ST7789 on S3 Super Mini requires color inversion + _tft_instance.invertDisplay(true); // ST7789 on S3 Super Mini requires color inversion #endif Serial.println("Display: tft.init() done"); #endif // HEADLESS diff --git a/src/display_ui.h b/src/display_ui.h index a9a78f6..30f33aa 100644 --- a/src/display_ui.h +++ b/src/display_ui.h @@ -17,9 +17,9 @@ enum ScreenState { SCREEN_OTA_UPDATE }; -extern lgfx::LGFX_Device* tft_ptr; +extern lgfx::LovyanGFX* tft_ptr; // Convenience reference — all callers use `tft.method()` unchanged -extern lgfx::LGFX_Device& tft; +extern lgfx::LovyanGFX& tft; void initDisplay(); void updateDisplay(); diff --git a/src/icons.h b/src/icons.h index 38f02d1..396aa21 100644 --- a/src/icons.h +++ b/src/icons.h @@ -265,7 +265,7 @@ const uint8_t PROGMEM icon_lightning[] = { }; // Helper: draw a 16x16 1-bit icon at (x, y) with given color, transparent bg -inline void drawIcon16(lgfx::LGFX_Device& tft, int16_t x, int16_t y, +inline void drawIcon16(lgfx::LovyanGFX& tft, int16_t x, int16_t y, const uint8_t* icon, uint16_t color) { for (int row = 0; row < 16; row++) { uint8_t b0 = pgm_read_byte(&icon[row * 2]); @@ -280,7 +280,7 @@ inline void drawIcon16(lgfx::LGFX_Device& tft, int16_t x, int16_t y, } // Helper: draw a 32x32 1-bit icon at (x, y) with given color, transparent bg -inline void drawIcon32(lgfx::LGFX_Device& tft, int16_t x, int16_t y, +inline void drawIcon32(lgfx::LovyanGFX& tft, int16_t x, int16_t y, const uint8_t* icon, uint16_t color) { for (int row = 0; row < 32; row++) { uint32_t bits = ((uint32_t)pgm_read_byte(&icon[row * 4]) << 24) | diff --git a/src/web_server.cpp b/src/web_server.cpp index 3d40d72..0ed8134 100644 --- a/src/web_server.cpp +++ b/src/web_server.cpp @@ -2376,48 +2376,41 @@ void initWebServer() { } int w = headlessSprite.width(); int h = headlessSprite.height(); - // BMP header (54 bytes): BITMAPFILEHEADER + BITMAPINFOHEADER - // RGB565 pixels stored as BGR24 in BMP int rowSize = ((w * 3 + 3) & ~3); // padded to 4-byte boundary int imageSize = rowSize * h; int fileSize = 54 + imageSize; - uint8_t* bmp = (uint8_t*)malloc(fileSize); - if (!bmp) { - server.send(500, "text/plain", "OOM"); - return; - } - memset(bmp, 0, 54); - // BITMAPFILEHEADER - bmp[0] = 'B'; bmp[1] = 'M'; - bmp[2] = fileSize & 0xFF; bmp[3] = (fileSize >> 8) & 0xFF; - bmp[4] = (fileSize >> 16) & 0xFF; bmp[5] = (fileSize >> 24) & 0xFF; - bmp[10] = 54; // pixel data offset - // BITMAPINFOHEADER - bmp[14] = 40; // header size - bmp[18] = w & 0xFF; bmp[19] = (w >> 8) & 0xFF; - // height negative = top-down + + // Build 54-byte BMP header + uint8_t hdr[54] = {0}; + hdr[0] = 'B'; hdr[1] = 'M'; + hdr[2] = fileSize; hdr[3] = fileSize >> 8; + hdr[4] = fileSize >> 16; hdr[5] = fileSize >> 24; + hdr[10] = 54; + hdr[14] = 40; + hdr[18] = w; hdr[19] = w >> 8; int negH = -h; - bmp[22] = negH & 0xFF; bmp[23] = (negH >> 8) & 0xFF; - bmp[24] = (negH >> 16) & 0xFF; bmp[25] = (negH >> 24) & 0xFF; - bmp[26] = 1; // planes - bmp[28] = 24; // bits per pixel (BGR24) - bmp[34] = imageSize & 0xFF; bmp[35] = (imageSize >> 8) & 0xFF; - bmp[36] = (imageSize >> 16) & 0xFF; bmp[37] = (imageSize >> 24) & 0xFF; - // Pixel data: BGR24, top-down (negative height) - uint8_t* px = bmp + 54; + hdr[22] = negH; hdr[23] = negH >> 8; hdr[24] = negH >> 16; hdr[25] = negH >> 24; + hdr[26] = 1; + hdr[28] = 24; + hdr[34] = imageSize; hdr[35] = imageSize >> 8; + hdr[36] = imageSize >> 16; hdr[37] = imageSize >> 24; + + // Stream: header first, then one row at a time + server.setContentLength(fileSize); + server.send(200, "image/bmp", ""); + server.sendContent((const char*)hdr, 54); + + uint8_t rowBuf[((240 * 3 + 3) & ~3)]; // stack buffer for one row for (int y = 0; y < h; y++) { - uint8_t* row = px + y * rowSize; + memset(rowBuf, 0, rowSize); for (int x = 0; x < w; x++) { uint16_t c = headlessSprite.readPixel(x, y); - row[x*3+0] = (c & 0x001F) << 3; // B - row[x*3+1] = ((c >> 5) & 0x3F) << 2; // G - row[x*3+2] = ((c >> 11) & 0x1F) << 3; // R + rowBuf[x*3+0] = (c & 0x001F) << 3; // B + rowBuf[x*3+1] = ((c >> 5) & 0x3F) << 2; // G + rowBuf[x*3+2] = ((c >> 11) & 0x1F) << 3; // R } + server.sendContent((const char*)rowBuf, rowSize); } - server.setContentLength(fileSize); - server.send(200, "image/bmp", ""); - server.sendContent((const char*)bmp, fileSize); - free(bmp); }); server.on("/display", HTTP_GET, []() { From f7772f5d36d129ae527add352f25c6c51c552983 Mon Sep 17 00:00:00 2001 From: Niels Timmer Date: Wed, 1 Apr 2026 07:52:29 +0200 Subject: [PATCH 6/6] Shrink headless sprite to layout size, fix root page OOM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sprite now uses LY_W×LY_H (240×240) instead of hardcoded 240×280, saving 19KB of SRAM on the C3 - This frees enough heap for the ~59KB settings page to load at / - Use LY_W/LY_H in screenshot rowBuf and /display HTML dimensions - Add layout.h include to web_server.cpp --- src/display_ui.cpp | 4 ++-- src/web_server.cpp | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/display_ui.cpp b/src/display_ui.cpp index a3742ca..6109830 100644 --- a/src/display_ui.cpp +++ b/src/display_ui.cpp @@ -244,9 +244,9 @@ void initDisplay() { #endif // HEADLESS #ifdef HEADLESS headlessSprite.setColorDepth(16); - headlessSprite.createSprite(240, 280); + headlessSprite.createSprite(LY_W, LY_H); headlessSpriteReady = true; - Serial.println("HEADLESS mode: sprite framebuffer ready (240x280)"); + Serial.printf("HEADLESS mode: sprite framebuffer ready (%dx%d)\n", LY_W, LY_H); #endif #if defined(DISPLAY_CYD) // Clear entire GRAM at rotation 0 first (guarantees all 240x320 pixels diff --git a/src/web_server.cpp b/src/web_server.cpp index 0ed8134..8523427 100644 --- a/src/web_server.cpp +++ b/src/web_server.cpp @@ -6,6 +6,7 @@ #include "wifi_manager.h" #include "display_ui.h" #include "config.h" +#include "layout.h" #include "button.h" #include "buzzer.h" #include "timezones.h" @@ -2400,7 +2401,7 @@ void initWebServer() { server.send(200, "image/bmp", ""); server.sendContent((const char*)hdr, 54); - uint8_t rowBuf[((240 * 3 + 3) & ~3)]; // stack buffer for one row + uint8_t rowBuf[((LY_W * 3 + 3) & ~3)]; // stack buffer for one row for (int y = 0; y < h; y++) { memset(rowBuf, 0, rowSize); for (int x = 0; x < w; x++) { @@ -2414,12 +2415,15 @@ void initWebServer() { }); server.on("/display", HTTP_GET, []() { - server.send(200, "text/html", + char html[350]; + snprintf(html, sizeof(html), "BambuHelper Display" "" "" - ""); + "", + LY_W, LY_H); + server.send(200, "text/html", html); }); #endif server.onNotFound(handleNotFound);