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/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..25aad2b 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 @@ -101,24 +62,30 @@ platform = espressif32 board = lolin_c3_mini framework = arduino monitor_speed = 115200 -board_build.partitions = partitions_4mb.csv -extra_scripts = pre:scripts/patch_spi_for_esp32-c3.py +board_build.partitions = partitions_4mb_c3.csv 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 + -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 - ; --- 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 + +; ============================================================================= +; 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/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..a41fb1a 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::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; 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::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(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::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(TFT_eSPI& 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; @@ -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..64a72ce 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::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(TFT_eSPI& 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(TFT_eSPI& 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(TFT_eSPI& 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 f223415..8461459 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::LovyanGFX& 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::LovyanGFX& 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::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. + // 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::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); @@ -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::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; @@ -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::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, @@ -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::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 3efd41b..53e5263 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::LovyanGFX& 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::LovyanGFX& 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::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(TFT_eSPI& 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(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::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 e48f1f0..6109830 100644 --- a/src/display_ui.cpp +++ b/src/display_ui.cpp @@ -13,7 +13,144 @@ #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 +#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::LovyanGFX& tft = *tft_ptr; // Use user-configured bg color instead of hardcoded CLR_BG #undef CLR_BG @@ -85,7 +222,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; @@ -97,10 +234,20 @@ void setBacklight(uint8_t level) { void initDisplay() { Serial.println("Display: pre-init delay..."); delay(500); +#ifndef HEADLESS Serial.println("Display: calling tft.init()..."); - Serial.flush(); - tft.init(); // TFT_eSPI configures SPI from build flags + _tft_instance.init(); // LovyanGFX configures SPI from the board class above +#if defined(BOARD_IS_S3) + _tft_instance.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(LY_W, LY_H); + headlessSpriteReady = true; + 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 // are addressed). Without this, rotations 1/3 leave 80px of uninitialized @@ -114,12 +261,13 @@ 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 -#if defined(BACKLIGHT_PIN) && BACKLIGHT_PIN >= 0 +#if defined(BACKLIGHT_PIN) && BACKLIGHT_PIN >= 0 && !defined(HEADLESS) pinMode(BACKLIGHT_PIN, OUTPUT); setBacklight(200); #endif @@ -296,7 +444,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); @@ -335,7 +482,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 @@ -1430,11 +1576,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..30f33aa 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::LovyanGFX* tft_ptr; +// Convenience reference — all callers use `tft.method()` unchanged +extern lgfx::LovyanGFX& tft; void initDisplay(); void updateDisplay(); diff --git a/src/icons.h b/src/icons.h index 05c8a2f..396aa21 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::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]); @@ -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::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/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(); diff --git a/src/web_server.cpp b/src/web_server.cpp index 319e66d..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" @@ -1077,6 +1078,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 +1158,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 +1195,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'); @@ -2360,6 +2366,65 @@ 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(); + int rowSize = ((w * 3 + 3) & ~3); // padded to 4-byte boundary + int imageSize = rowSize * h; + int fileSize = 54 + imageSize; + + // 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; + 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[((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++) { + uint16_t c = headlessSprite.readPixel(x, y); + 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.on("/display", HTTP_GET, []() { + char html[350]; + snprintf(html, sizeof(html), + "BambuHelper Display" + "" + "" + "", + LY_W, LY_H); + server.send(200, "text/html", html); + }); #endif server.onNotFound(handleNotFound); server.begin();