diff --git a/.gitignore b/.gitignore index 3c06ffa..db7ffa0 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,7 @@ firmware/ *.map tools/pushall_dump.json tools/ams_dump.json + +# Vendor esp32s3 libs folder — 212 MB, copied locally during JC3248W535 +# driver work. Not needed for the build; kept outside the repo. +lib/arduino_esp32s3_libs_vendor/ diff --git a/include/layout.h b/include/layout.h index 6d1bf39..ae8fed2 100644 --- a/include/layout.h +++ b/include/layout.h @@ -6,7 +6,9 @@ // gauge positions, text positions, etc. // To add a new display: create layout_xxx.h and add an #elif here. -#if defined(DISPLAY_240x320) +#if defined(DISPLAY_320x480) + #include "layout_320x480.h" // 320x480 portrait (Guition JC3248W535) +#elif defined(DISPLAY_240x320) #include "layout_240x320.h" // 240x320 portrait (CYD, Waveshare) #else #include "layout_default.h" // ESP32-S3 Mini: ST7789 240x240 diff --git a/include/layout_320x480.h b/include/layout_320x480.h new file mode 100644 index 0000000..023a73b --- /dev/null +++ b/include/layout_320x480.h @@ -0,0 +1,134 @@ +#ifndef LAYOUT_320x480_H +#define LAYOUT_320x480_H + +// Layout profile: 320x480 portrait (Guition JC3248W535, AXS15231B QSPI IPS). +// Redesigned layout that uses the extra screen real estate — does not simply +// stretch the 240x320 layout. Gauges are larger, AMS strip is always visible, +// and the ETA / bottom status areas are generously sized. + +// --- Screen dimensions --- +#define LY_W 320 +#define LY_H 480 + +// --- LED progress bar (top, y=0) --- +#define LY_BAR_W 316 +#define LY_BAR_H 7 + +// --- Header bar --- +#define LY_HDR_Y 10 +#define LY_HDR_H 26 +#define LY_HDR_NAME_X 8 +#define LY_HDR_CY 23 // vertical center of header text +#define LY_HDR_BADGE_RX 10 // badge right margin from SCREEN_W +#define LY_HDR_DOT_CY 13 // multi-printer indicator dot Y + +// --- Printing: 2x3 gauge grid (3 columns, 2 rows) --- +// 320px wide split into 3 columns of ~107px — gauges are r=48, spacing tuned +// so left/right edges sit ~5px from the screen edges. +#define LY_GAUGE_R 48 // radius for all gauges (was 32 on 240x) +#define LY_GAUGE_T 9 // progress arc thickness (was 6 on 240x) +#define LY_COL1 56 +#define LY_COL2 160 +#define LY_COL3 264 +#define LY_ROW1 92 // top row center Y (gauge top edge y=44) +#define LY_ROW2 228 // bottom row center Y (gauge top edge y=180) + +// --- AMS tray visualization zone (below gauge grid) --- +// Row 2 gauges bottom edge is at y=276 (228+48). Labels extend ~12px below, +// so AMS starts at y=295 with 4px gap under it (ETA begins at y=355). +#define LY_AMS_Y 295 +#define LY_AMS_H 56 +#define LY_AMS_BAR_H 32 +#define LY_AMS_BAR_GAP 3 +#define LY_AMS_GROUP_GAP 10 +#define LY_AMS_LABEL_OFFY 4 +#define LY_AMS_MARGIN 10 +#define LY_AMS_BAR_MAX_W 42 + +// --- Printing: ETA / info zone --- +#define LY_ETA_Y 360 +#define LY_ETA_H 46 +#define LY_ETA_TEXT_Y 383 + +// --- Printing: bottom status bar --- +#define LY_BOT_Y 414 +#define LY_BOT_H 26 +#define LY_BOT_CY 427 + +// --- Printing: WiFi signal indicator --- +#define LY_WIFI_X 6 +#define LY_WIFI_Y 452 + +// --- Idle screen (with printer) --- +#define LY_IDLE_NAME_Y 45 +#define LY_IDLE_STATE_Y 75 +#define LY_IDLE_STATE_H 28 +#define LY_IDLE_STATE_TY 89 +#define LY_IDLE_DOT_Y 125 +#define LY_IDLE_GAUGE_R 46 +#define LY_IDLE_GAUGE_Y 210 +#define LY_IDLE_G_OFFSET 80 + +// --- Idle screen: AMS zone (below gauges) --- +#define LY_IDLE_AMS_Y 275 +#define LY_IDLE_AMS_H 80 +#define LY_IDLE_AMS_BAR_H 46 + +// --- Idle screen (no printer) --- +#define LY_IDLE_NP_TITLE_Y 60 +#define LY_IDLE_NP_WIFI_Y 120 +#define LY_IDLE_NP_DOT_Y 150 +#define LY_IDLE_NP_MSG_Y 200 +#define LY_IDLE_NP_OPEN_Y 240 +#define LY_IDLE_NP_IP_Y 290 + +// --- Finished screen (portrait, vertically centered) --- +#define LY_FIN_GAUGE_R 48 +#define LY_FIN_GL 96 +#define LY_FIN_GR 224 +#define LY_FIN_GY 150 +#define LY_FIN_TEXT_Y 245 +#define LY_FIN_FILE_Y 290 +#define LY_FIN_KWH_Y 320 +#define LY_FIN_AMS_Y 345 +#define LY_FIN_AMS_H 65 +#define LY_FIN_AMS_BAR_H 38 +#define LY_FIN_BOT_Y 436 +#define LY_FIN_BOT_H 28 +#define LY_FIN_WIFI_Y 458 + +// --- AP mode screen --- +#define LY_AP_TITLE_Y 60 +#define LY_AP_SSID_LBL_Y 120 +#define LY_AP_SSID_Y 160 +#define LY_AP_PASS_LBL_Y 210 +#define LY_AP_PASS_Y 240 +#define LY_AP_OPEN_Y 280 +#define LY_AP_IP_Y 315 + +// --- Simple clock (centered in 480px height) --- +#define LY_CLK_CLEAR_Y 110 +#define LY_CLK_CLEAR_H 280 +#define LY_CLK_TIME_Y 210 +#define LY_CLK_AMPM_Y 265 +#define LY_CLK_DATE_Y 310 + +// --- Pong/Breakout clock (scaled for 320x480) --- +#define LY_ARK_BRICK_ROWS 5 +#define LY_ARK_COLS 10 +#define LY_ARK_BRICK_W 30 // 10 cols * 30 + 9 gaps * 2 = 318 (fits 320) +#define LY_ARK_BRICK_H 12 +#define LY_ARK_BRICK_GAP 2 +#define LY_ARK_START_X 1 +#define LY_ARK_START_Y 40 +#define LY_ARK_PADDLE_Y 460 +#define LY_ARK_PADDLE_W 44 +#define LY_ARK_TIME_Y 220 +#define LY_ARK_DATE_Y 10 +#define LY_ARK_DIGIT_W 42 +#define LY_ARK_DIGIT_H 64 +#define LY_ARK_COLON_W 16 +#define LY_ARK_DATE_CLR_X 50 +#define LY_ARK_DATE_CLR_W 220 + +#endif // LAYOUT_320x480_H diff --git a/partitions_16mb.csv b/partitions_16mb.csv new file mode 100644 index 0000000..bf19e7c --- /dev/null +++ b/partitions_16mb.csv @@ -0,0 +1,11 @@ +# Partition table for 16 MB flash (Guition JC3248W535 — ESP32-S3-N16R8) +# Dual OTA app slots of 6.25 MB each — large headroom for future features. +# SPIFFS reserved at end but unused (settings stored in NVS). +# 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, 0x640000, +app1, app, ota_1, 0x650000, 0x640000, +spiffs, data, spiffs, 0xc90000, 0x360000, diff --git a/platformio.ini b/platformio.ini index 0e01132..99a5d18 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,10 +1,11 @@ ; BambuHelper - Bambu Lab Printer Monitor -; Supports ESP32-S3 Super Mini, ESP32-2432S028 (CYD), ESP32-C3 Super Mini +; Supports ESP32-S3 Super Mini, ESP32-2432S028 (CYD), ESP32-C3 Super Mini, +; Waveshare ESP32-S3-Touch-LCD-2 / -1.54, Guition JC3248W535 ; --- Shared library dependencies --- [common] lib_deps = - lovyan03/LovyanGFX@^1.1.16 + lovyan03/LovyanGFX@^1.2.19 knolleary/PubSubClient@^2.8 bblanchon/ArduinoJson@^7.0 @@ -128,6 +129,47 @@ build_flags = -D CST816_RST=47 -D CST816_IRQ=48 +; ============================================================================= +; Guition JC3248W535 - 3.5" 320x480 IPS (AXS15231B QSPI + I2C touch) +; https://www.guition.com/ — ESP32-S3-N16R8, 16MB flash / 8MB PSRAM. +; Display driver wraps Arduino_GFX's Arduino_AXS15231B inside a LovyanGFX +; Panel_Device subclass (src/lgfx_panel_axs15231b_agfx.hpp), since +; mainline LovyanGFX does not ship Panel_AXS15231B. +; +; Touch RST/INT pins are not wired on the shipped Guition boards inspected — +; the touch IC shares reset with the display and is polled (no INT). If your +; board revision exposes these pins, add AXS_TOUCH_RST / AXS_TOUCH_IRQ here. +; ============================================================================= +[env:jc3248w535] +platform = espressif32 +board = esp32-s3-devkitc-1 +framework = arduino +monitor_speed = 115200 +board_build.partitions = partitions_16mb.csv +board_build.arduino.memory_type = qio_opi +board_upload.flash_size = 16MB +board_upload.maximum_size = 16777216 +board_build.f_flash = 80000000L +board_build.flash_mode = qio +lib_deps = + ${common.lib_deps} + moononournation/GFX Library for Arduino@~1.5.0 +build_flags = + -D BOARD_VARIANT=\"jc3248w535\" + -D BOARD_IS_JC3248W535=1 + -D ENABLE_OTA_AUTO=1 + -D BOARD_HAS_PSRAM=1 + -D ARDUINO_USB_CDC_ON_BOOT=1 + ; --- Display (AXS15231B QSPI, 320x480 portrait native) --- + ; Pins are hard-coded inside Panel_AXS15231B_AGFX (src/lgfx_panel_axs15231b_agfx.hpp) + ; because Arduino_GFX's databus takes pins at construction, not at begin(). + -D DISPLAY_320x480=1 + -D BACKLIGHT_PIN=1 + ; --- Touch (AXS15231B touch controller on I2C @0x3B) --- + -D USE_AXS_TOUCH=1 + -D AXS_TOUCH_SDA=4 + -D AXS_TOUCH_SCL=8 + ; ============================================================================= ; ESP32-C3 Super Mini + ST7789 240x240 ; ============================================================================= diff --git a/src/button.cpp b/src/button.cpp index 76066c9..d42cfbc 100644 --- a/src/button.cpp +++ b/src/button.cpp @@ -1,5 +1,6 @@ #include "button.h" #include "settings.h" +#include "buzzer.h" #if defined(USE_XPT2046) #include @@ -27,6 +28,42 @@ value = Wire.read(); return true; } +#elif defined(USE_AXS_TOUCH) + // AXS15231B integrated touch controller. I2C slave at 0x3B. + // Protocol (per axs15231b-lovyangfx skill): write 8-byte command, read 8 + // bytes back. Touch is active when rx[0] == 0 (no gesture) AND rx[1] != 0 + // (finger count > 0). Coordinates arrive pre-scaled to panel resolution, + // NOT raw 0-4095. Single-touch only. + #include + #define AXS_TOUCH_ADDR 0x3B + static const uint8_t AXS_READ_TOUCHPAD_CMD[8] = { + 0xB5, 0xAB, 0xA5, 0x5A, 0x00, 0x00, 0x00, 0x08 + }; + static bool axsTouchBusReady = false; + static bool axsTouchSeen = false; + + static bool axsTouchProbe() { + Wire.beginTransmission(AXS_TOUCH_ADDR); + return Wire.endTransmission(true) == 0; + } + + // Returns true if a touch is active and fills x/y with panel-scaled + // coordinates (native portrait 320x480 — caller applies rotation). + static bool axsTouchRead(uint16_t& x, uint16_t& y) { + Wire.beginTransmission(AXS_TOUCH_ADDR); + Wire.write(AXS_READ_TOUCHPAD_CMD, sizeof(AXS_READ_TOUCHPAD_CMD)); + if (Wire.endTransmission() != 0) return false; + uint8_t rx[8] = {0}; + size_t got = Wire.requestFrom((uint8_t)AXS_TOUCH_ADDR, (uint8_t)8); + if (got < 6) return false; + for (size_t i = 0; i < got && i < sizeof(rx); ++i) rx[i] = Wire.read(); + // Valid plain-touch: gesture=0 and at least one finger down. + if (rx[0] != 0) return false; + if ((rx[1] & 0x0F) == 0) return false; + x = ((uint16_t)(rx[2] & 0x0F) << 8) | rx[3]; + y = ((uint16_t)(rx[4] & 0x0F) << 8) | rx[5]; + return true; + } #elif defined(TOUCH_CS) #include "display_ui.h" // extern tft for getTouch() #endif @@ -36,8 +73,50 @@ static bool stableState = false; static unsigned long lastChangeMs = 0; static const unsigned long DEBOUNCE_MS = 50; +void sanitizeButtonPin() { + // Only the GPIO-backed button types use buttonPin. Touchscreen talks over + // a bus defined elsewhere and has no single pin to conflict. + if (buttonType != BTN_PUSH && buttonType != BTN_TOUCH) return; + if (buttonPin == 0) return; + + auto clash = [&](const char* what) { + Serial.printf("Button: pin %u conflicts with %s, disabling\n", + (unsigned)buttonPin, what); + buttonPin = 0; + }; + +#if defined(BACKLIGHT_PIN) && BACKLIGHT_PIN >= 0 + if (buttonPin == BACKLIGHT_PIN) { clash("backlight"); return; } +#endif +#if defined(USE_AXS_TOUCH) + if (buttonPin == AXS_TOUCH_SDA) { clash("AXS touch SDA"); return; } + if (buttonPin == AXS_TOUCH_SCL) { clash("AXS touch SCL"); return; } +#endif +#if defined(USE_CST816) + if (buttonPin == CST816_SDA) { clash("CST816 touch SDA"); return; } + if (buttonPin == CST816_SCL) { clash("CST816 touch SCL"); return; } + #if defined(CST816_IRQ) + if (buttonPin == CST816_IRQ) { clash("CST816 touch IRQ"); return; } + #endif + #if defined(CST816_RST) + if (buttonPin == CST816_RST) { clash("CST816 touch RST"); return; } + #endif +#endif +#if defined(USE_XPT2046) + if (buttonPin == TOUCH_CS) { clash("XPT2046 CS"); return; } + if (buttonPin == TOUCH_IRQ) { clash("XPT2046 IRQ"); return; } + if (buttonPin == TOUCH_MOSI) { clash("XPT2046 MOSI"); return; } + if (buttonPin == TOUCH_MISO) { clash("XPT2046 MISO"); return; } + if (buttonPin == TOUCH_CLK) { clash("XPT2046 CLK"); return; } +#endif + if (buzzerSettings.pin != 0 && buttonPin == buzzerSettings.pin) { + clash("buzzer"); return; + } +} + void initButton() { if (buttonType == BTN_DISABLED) return; + sanitizeButtonPin(); #if defined(USE_XPT2046) if (buttonType == BTN_TOUCHSCREEN) { touchSPI.begin(TOUCH_CLK, TOUCH_MISO, TOUCH_MOSI, TOUCH_CS); @@ -75,6 +154,21 @@ void initButton() { } return; } +#elif defined(USE_AXS_TOUCH) + if (buttonType == BTN_TOUCHSCREEN) { + Wire.begin(AXS_TOUCH_SDA, AXS_TOUCH_SCL); + Wire.setClock(400000); + axsTouchBusReady = true; + if (axsTouchProbe()) { + Serial.printf("AXS15231B touch initialized (I2C SDA=%d SCL=%d, addr 0x%02X)\n", + AXS_TOUCH_SDA, AXS_TOUCH_SCL, AXS_TOUCH_ADDR); + axsTouchSeen = true; + } else { + Serial.printf("AXS15231B touch did not answer at init (addr 0x%02X, SDA=%d SCL=%d); will keep retrying at runtime\n", + AXS_TOUCH_ADDR, AXS_TOUCH_SDA, AXS_TOUCH_SCL); + } + return; + } #endif if (buttonType == BTN_TOUCHSCREEN) return; if (buttonPin == 0) return; @@ -105,6 +199,14 @@ bool wasButtonPressed() { cst816Seen = true; } raw = (touchNum > 0); +#elif defined(USE_AXS_TOUCH) + if (!axsTouchBusReady) return false; + uint16_t tx = 0, ty = 0; + raw = axsTouchRead(tx, ty); + if (raw && !axsTouchSeen) { + Serial.printf("AXS15231B touch became responsive at runtime (addr 0x%02X)\n", AXS_TOUCH_ADDR); + axsTouchSeen = true; + } #elif defined(TOUCH_CS) uint16_t tx, ty; raw = tft.getTouch(&tx, &ty); diff --git a/src/button.h b/src/button.h index fb3244b..1790326 100644 --- a/src/button.h +++ b/src/button.h @@ -5,5 +5,8 @@ void initButton(); bool wasButtonPressed(); // returns true once per press (edge-detected, debounced) +void sanitizeButtonPin(); // zero buttonPin if it conflicts with a reserved + // subsystem (backlight, touch bus, buzzer). No-op + // for touchscreen type. #endif // BUTTON_H diff --git a/src/display_anim.cpp b/src/display_anim.cpp index a41fb1a..666aafe 100644 --- a/src/display_anim.cpp +++ b/src/display_anim.cpp @@ -12,18 +12,18 @@ // --------------------------------------------------------------------------- static uint16_t spinnerAngle = 0; -void drawSpinner(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawSpinner(lgfx::LovyanGFX& gfx, 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.drawArc(cx, cy, radius, radius - 4, + gfx.drawArc(cx, cy, radius, radius - 4, prevStart, prevEnd, CLR_BG); } else { - tft.drawArc(cx, cy, radius, radius - 4, + gfx.drawArc(cx, cy, radius, radius - 4, prevStart, 360, CLR_BG); - tft.drawArc(cx, cy, radius, radius - 4, + gfx.drawArc(cx, cy, radius, radius - 4, 0, prevEnd, CLR_BG); } @@ -34,12 +34,12 @@ void drawSpinner(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, // Draw arc segment (handle wrap-around) if (arcEnd > arcStart) { - tft.drawArc(cx, cy, radius, radius - 4, + gfx.drawArc(cx, cy, radius, radius - 4, arcStart, arcEnd, color); } else { - tft.drawArc(cx, cy, radius, radius - 4, + gfx.drawArc(cx, cy, radius, radius - 4, arcStart, 360, color); - tft.drawArc(cx, cy, radius, radius - 4, + gfx.drawArc(cx, cy, radius, radius - 4, 0, arcEnd, color); } } @@ -47,27 +47,27 @@ void drawSpinner(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, // --------------------------------------------------------------------------- // Animated dots "..." // --------------------------------------------------------------------------- -void drawAnimDots(lgfx::LovyanGFX& tft, int16_t x, int16_t y, uint16_t color) { +void drawAnimDots(lgfx::LovyanGFX& gfx, int16_t x, int16_t y, uint16_t color) { unsigned long ms = millis(); int phase = (ms / 400) % 4; - tft.setTextFont(2); - tft.setTextDatum(TL_DATUM); + gfx.setTextFont(2); + gfx.setTextDatum(TL_DATUM); for (int i = 0; i < 3; i++) { uint16_t dotColor = (i < phase) ? color : CLR_TEXT_DARK; - tft.setTextColor(dotColor, CLR_BG); - tft.drawString(".", x + i * 8, y); + gfx.setTextColor(dotColor, CLR_BG); + gfx.drawString(".", x + i * 8, y); } } // --------------------------------------------------------------------------- // Indeterminate slide bar — a glowing segment slides back and forth // --------------------------------------------------------------------------- -void drawSlideBar(lgfx::LovyanGFX& tft, int16_t x, int16_t y, int16_t w, int16_t h, +void drawSlideBar(lgfx::LovyanGFX& gfx, 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); + gfx.fillRoundRect(x, y, w, h, h / 2, trackColor); // Segment: 25% of bar width, bounces smoothly using sine const int16_t segW = w / 4; @@ -75,7 +75,7 @@ void drawSlideBar(lgfx::LovyanGFX& tft, int16_t x, int16_t y, int16_t w, int16_t float pos = (sinf(t * 2.0f * PI - PI / 2.0f) + 1.0f) / 2.0f; // 0..1 int16_t segX = x + (int16_t)(pos * (float)(w - segW)); - tft.fillRoundRect(segX, y, segW, h, h / 2, color); + gfx.fillRoundRect(segX, y, segW, h, h / 2, color); } // --------------------------------------------------------------------------- @@ -95,7 +95,7 @@ static unsigned long completionStart = 0; static bool completionDone = false; static int16_t prevRing = 0; -void drawCompletionAnim(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, bool reset) { +void drawCompletionAnim(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, bool reset) { if (reset) { completionStart = millis(); completionDone = false; @@ -113,22 +113,22 @@ void drawCompletionAnim(lgfx::LovyanGFX& 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.drawArc(cx, cy, prevRing, prevRing - 3, 0, 360, CLR_BG); + gfx.drawArc(cx, cy, prevRing, prevRing - 3, 0, 360, CLR_BG); } - tft.drawArc(cx, cy, r, r - 3, 0, 360, CLR_GREEN); + gfx.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.drawArc(cx, cy, finalR, finalR - 4, 0, 360, CLR_GREEN); + gfx.drawArc(cx, cy, finalR, finalR - 4, 0, 360, CLR_GREEN); } // Phase 3 (600ms+): static ring + large checkmark, done else { - tft.drawArc(cx, cy, finalR, finalR - 4, 0, 360, CLR_GREEN); + gfx.drawArc(cx, cy, finalR, finalR - 4, 0, 360, CLR_GREEN); // Clear center for checkmark - tft.fillCircle(cx, cy, finalR - 5, CLR_BG); + gfx.fillCircle(cx, cy, finalR - 5, CLR_BG); // Draw 32x32 checkmark centered - drawIcon32(tft, cx - 16, cy - 16, icon_check_32, CLR_GREEN); + drawIcon32(gfx, cx - 16, cy - 16, icon_check_32, CLR_GREEN); completionDone = true; } } diff --git a/src/display_anim.h b/src/display_anim.h index 64a72ce..6da0d9e 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::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawSpinner(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, uint16_t color); // Animated dots "..." that cycle (call each frame, uses millis()) -void drawAnimDots(lgfx::LovyanGFX& tft, int16_t x, int16_t y, uint16_t color); +void drawAnimDots(lgfx::LovyanGFX& gfx, 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::LovyanGFX& tft, int16_t x, int16_t y, int16_t w, int16_t h, +void drawSlideBar(lgfx::LovyanGFX& gfx, 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::LovyanGFX& tft, int16_t cx, int16_t cy, bool reset); +void drawCompletionAnim(lgfx::LovyanGFX& gfx, 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 bd306fa..78dd9bb 100644 --- a/src/display_gauges.cpp +++ b/src/display_gauges.cpp @@ -28,8 +28,8 @@ class ScopedWrite { // --------------------------------------------------------------------------- // H2-style LED progress bar // --------------------------------------------------------------------------- -void drawLedProgressBar(lgfx::LovyanGFX& tft, int16_t y, uint8_t progress) { - ScopedWrite sw(tft); +void drawLedProgressBar(lgfx::LovyanGFX& gfx, int16_t y, uint8_t progress) { + ScopedWrite sw(gfx); uint16_t bg = dispSettings.bgColor; uint16_t track = dispSettings.trackColor; @@ -37,7 +37,7 @@ void drawLedProgressBar(lgfx::LovyanGFX& tft, int16_t y, uint8_t progress) { const int16_t barH = LY_BAR_H; const int16_t barX = (SCREEN_W - barW) / 2; - tft.fillRect(barX, y, barW, barH, bg); + gfx.fillRect(barX, y, barW, barH, bg); if (progress == 0) return; @@ -46,17 +46,17 @@ void drawLedProgressBar(lgfx::LovyanGFX& tft, int16_t y, uint8_t progress) { uint16_t barColor = dispSettings.progress.arc; - tft.fillRoundRect(barX, y, fillW, barH, 2, barColor); + gfx.fillRoundRect(barX, y, fillW, barH, 2, barColor); uint16_t glowColor = alphaBlend565(160, CLR_TEXT, barColor); - tft.drawFastHLine(barX + 1, y + barH / 2, fillW - 2, glowColor); + gfx.drawFastHLine(barX + 1, y + barH / 2, fillW - 2, glowColor); if (fillW > 4 && progress < 100) { - tft.fillRect(barX + fillW - 3, y, 3, barH, glowColor); + gfx.fillRect(barX + fillW - 3, y, 3, barH, glowColor); } if (fillW < barW) { - tft.fillRoundRect(barX + fillW, y, barW - fillW, barH, 2, track); + gfx.fillRoundRect(barX + fillW, y, barW - fillW, barH, 2, track); } } @@ -73,7 +73,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::LovyanGFX& tft, int16_t y, uint8_t progress, bool printing) { +void tickProgressShimmer(lgfx::LovyanGFX& gfx, int16_t y, uint8_t progress, bool printing) { if (!dispSettings.animatedBar || !printing || progress == 0) return; unsigned long now = millis(); @@ -96,7 +96,7 @@ void tickProgressShimmer(lgfx::LovyanGFX& tft, int16_t y, uint8_t progress, bool uint16_t barColor = dispSettings.progress.arc; - ScopedWrite sw_(tft); + ScopedWrite sw_(gfx); // Erase previous shimmer position (redraw base bar segment) if (shimmerPos > 0) { @@ -104,7 +104,7 @@ void tickProgressShimmer(lgfx::LovyanGFX& tft, int16_t y, uint8_t progress, bool int16_t eraseW = SHIMMER_STEP; if (eraseX < barX) { eraseW -= (barX - eraseX); eraseX = barX; } if (eraseW > 0) { - tft.fillRect(eraseX, y, eraseW, barH, barColor); + gfx.fillRect(eraseX, y, eraseW, barH, barColor); } } @@ -118,11 +118,11 @@ void tickProgressShimmer(lgfx::LovyanGFX& tft, int16_t y, uint8_t progress, bool uint16_t mid = alphaBlend565(100, CLR_TEXT, barColor); // Edge pixels if (sw >= 3) { - tft.fillRect(sx, y, 2, barH, mid); - tft.fillRect(sx + 2, y, sw - 4 > 0 ? sw - 4 : 1, barH, bright); - if (sw > 4) tft.fillRect(sx + sw - 2, y, 2, barH, mid); + gfx.fillRect(sx, y, 2, barH, mid); + gfx.fillRect(sx + 2, y, sw - 4 > 0 ? sw - 4 : 1, barH, bright); + if (sw > 4) gfx.fillRect(sx + sw - 2, y, 2, barH, mid); } else { - tft.fillRect(sx, y, sw, barH, bright); + gfx.fillRect(sx, y, sw, barH, bright); } } @@ -133,10 +133,10 @@ void tickProgressShimmer(lgfx::LovyanGFX& tft, int16_t y, uint8_t progress, bool // Restore the tail int16_t tailX = barX + fillW - SHIMMER_W - SHIMMER_STEP; if (tailX < barX) tailX = barX; - tft.fillRect(tailX, y, barX + fillW - tailX, barH, barColor); + gfx.fillRect(tailX, y, barX + fillW - tailX, barH, barColor); // Re-draw center glow line uint16_t glowColor = alphaBlend565(160, CLR_TEXT, barColor); - tft.drawFastHLine(barX + 1, y + barH / 2, fillW - 2, glowColor); + gfx.drawFastHLine(barX + 1, y + barH / 2, fillW - 2, glowColor); shimmerPos = 0; shimmerPaused = true; @@ -147,7 +147,7 @@ void tickProgressShimmer(lgfx::LovyanGFX& tft, int16_t y, uint8_t progress, bool // --------------------------------------------------------------------------- // Helper: draw arc track + fill, handling decrease properly // --------------------------------------------------------------------------- -static void drawArcFillLegacy(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, +static void drawArcFillLegacy(lgfx::LovyanGFX& gfx, 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. @@ -166,15 +166,15 @@ static void drawArcFillLegacy(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, float la1 = (float)((a1 + 90u) % 360u); if (la0 > la1) { // Arc crosses the 0° boundary — split into two segments - tft.drawArc(cx, cy, radius, radius - thickness, la0, 360.0f, color); - tft.drawArc(cx, cy, radius, radius - thickness, 0.0f, la1, color); + gfx.drawArc(cx, cy, radius, radius - thickness, la0, 360.0f, color); + gfx.drawArc(cx, cy, radius, radius - thickness, 0.0f, la1, color); } else { - tft.drawArc(cx, cy, radius, radius - thickness, la0, la1, color); + gfx.drawArc(cx, cy, radius, radius - thickness, la0, la1, color); } }; if (forceRedraw) { - tft.fillCircle(cx, cy, radius + 2, bg); + gfx.fillCircle(cx, cy, radius + 2, bg); arcDraw(startAngle, endAngle, track); } @@ -218,7 +218,7 @@ static inline float wedgeLineDistanceAA(float xpax, float ypay, return sqrtf(dx * dx + dy * dy) + h * dr; } -static void drawWedgeLineAA(lgfx::LovyanGFX& tft, +static void drawWedgeLineAA(lgfx::LovyanGFX& gfx, float ax, float ay, float bx, float by, float ar, float br, uint16_t fg_color, uint16_t bg_color) { @@ -234,8 +234,8 @@ static void drawWedgeLineAA(lgfx::LovyanGFX& tft, int32_t y0 = (int32_t)floorf(fminf(ay - ar, by - br)); int32_t y1 = (int32_t) ceilf(fmaxf(ay + ar, by + br)); - const int32_t maxX = (int32_t)tft.width() - 1; - const int32_t maxY = (int32_t)tft.height() - 1; + const int32_t maxX = (int32_t)gfx.width() - 1; + const int32_t maxY = (int32_t)gfx.height() - 1; if (x1 < 0 || y1 < 0 || x0 > maxX || y0 > maxY) return; if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0; @@ -254,12 +254,12 @@ static void drawWedgeLineAA(lgfx::LovyanGFX& tft, const float alpha = aaRadius - wedgeLineDistanceAA(xpax, ypay, bax, bay, rdt); if (alpha <= loAlphaTheshold) continue; if (alpha > hiAlphaTheshold) { - tft.drawPixel(xp, yp, fg_color); + gfx.drawPixel(xp, yp, fg_color); continue; } const uint8_t blendAlpha = (uint8_t)(alpha * pixelAlphaGain); if (blendAlpha == 0) continue; - tft.drawPixel(xp, yp, alphaBlend565(blendAlpha, fg_color, bg_color)); + gfx.drawPixel(xp, yp, alphaBlend565(blendAlpha, fg_color, bg_color)); } } } @@ -267,7 +267,7 @@ static void drawWedgeLineAA(lgfx::LovyanGFX& tft, // Scan-quadrant AA annulus slice. Port of TFT_eSPI::drawArc (smooth=true). // Angles: 0°=6 o'clock, clockwise, range 0-360. r=outer, ir=inner (inclusive). // Ends are NOT anti-aliased — caller adds radial AA wedges for smooth ends. -static void drawArcAA(lgfx::LovyanGFX& tft, int32_t x, int32_t y, +static void drawArcAA(lgfx::LovyanGFX& gfx, int32_t x, int32_t y, int32_t r, int32_t ir, uint32_t startAngle, uint32_t endAngle, uint16_t fg_color, uint16_t bg_color) { @@ -279,7 +279,7 @@ static void drawArcAA(lgfx::LovyanGFX& tft, int32_t x, int32_t y, if (r <= 0 || ir < 0) return; if (endAngle < startAngle) { - if (startAngle < 360) drawArcAA(tft, x, y, r, ir, startAngle, 360, fg_color, bg_color); + if (startAngle < 360) drawArcAA(gfx, x, y, r, ir, startAngle, 360, fg_color, bg_color); if (endAngle == 0) return; startAngle = 0; } @@ -354,48 +354,48 @@ static void drawArcAA(lgfx::LovyanGFX& tft, int32_t x, int32_t y, if (alpha < 16) continue; uint16_t pcol = alphaBlend565(alpha, fg_color, bg_color); slope = ((r - cy) << 16) / (r - cx); - if (slope <= startSlope[0] && slope >= endSlope[0]) tft.drawPixel(x + cx - r, y - cy + r, pcol); - if (slope >= startSlope[1] && slope <= endSlope[1]) tft.drawPixel(x + cx - r, y + cy - r, pcol); - if (slope <= startSlope[2] && slope >= endSlope[2]) tft.drawPixel(x - cx + r, y + cy - r, pcol); - if (slope <= endSlope[3] && slope >= startSlope[3]) tft.drawPixel(x - cx + r, y - cy + r, pcol); + if (slope <= startSlope[0] && slope >= endSlope[0]) gfx.drawPixel(x + cx - r, y - cy + r, pcol); + if (slope >= startSlope[1] && slope <= endSlope[1]) gfx.drawPixel(x + cx - r, y + cy - r, pcol); + if (slope <= startSlope[2] && slope >= endSlope[2]) gfx.drawPixel(x - cx + r, y + cy - r, pcol); + if (slope <= endSlope[3] && slope >= startSlope[3]) gfx.drawPixel(x - cx + r, y - cy + r, pcol); } - if (len[0]) tft.drawFastHLine(x + xst[0] - len[0] + 1 - r, y - cy + r, len[0], fg_color); - if (len[1]) tft.drawFastHLine(x + xst[1] - len[1] + 1 - r, y + cy - r, len[1], fg_color); - if (len[2]) tft.drawFastHLine(x - xst[2] + r, y + cy - r, len[2], fg_color); - if (len[3]) tft.drawFastHLine(x - xst[3] + r, y - cy + r, len[3], fg_color); + if (len[0]) gfx.drawFastHLine(x + xst[0] - len[0] + 1 - r, y - cy + r, len[0], fg_color); + if (len[1]) gfx.drawFastHLine(x + xst[1] - len[1] + 1 - r, y + cy - r, len[1], fg_color); + if (len[2]) gfx.drawFastHLine(x - xst[2] + r, y + cy - r, len[2], fg_color); + if (len[3]) gfx.drawFastHLine(x - xst[3] + r, y - cy + r, len[3], fg_color); } - if (startAngle == 0 || endAngle == 360) tft.drawFastVLine(x, y + r - w, w, fg_color); - if (startAngle <= 90 && endAngle >= 90) tft.drawFastHLine(x - r + 1, y, w, fg_color); - if (startAngle <= 180 && endAngle >= 180) tft.drawFastVLine(x, y - r + 1, w, fg_color); - if (startAngle <= 270 && endAngle >= 270) tft.drawFastHLine(x + r - w, y, w, fg_color); + if (startAngle == 0 || endAngle == 360) gfx.drawFastVLine(x, y + r - w, w, fg_color); + if (startAngle <= 90 && endAngle >= 90) gfx.drawFastHLine(x - r + 1, y, w, fg_color); + if (startAngle <= 180 && endAngle >= 180) gfx.drawFastVLine(x, y - r + 1, w, fg_color); + if (startAngle <= 270 && endAngle >= 270) gfx.drawFastHLine(x + r - w, y, w, fg_color); } -static void drawArcCapAA(lgfx::LovyanGFX& tft, int32_t x, int32_t y, +static void drawArcCapAA(lgfx::LovyanGFX& gfx, int32_t x, int32_t y, int32_t r, int32_t ir, uint32_t angle, uint16_t fg_color, uint16_t bg_color) { constexpr float deg2rad = 3.14159265358979f / 180.0f; const float sx = -sinf(angle * deg2rad); const float sy = +cosf(angle * deg2rad); - drawWedgeLineAA(tft, + drawWedgeLineAA(gfx, sx * ir + x, sy * ir + y, sx * r + x, sy * r + y, 0.3f, 0.3f, fg_color, bg_color); } -static void drawArcSegmentAA(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, +static void drawArcSegmentAA(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, int16_t innerRadius, uint16_t a0, uint16_t a1, uint16_t fg_color, uint16_t bg_color, bool drawStartCap, bool drawEndCap, uint16_t startCapBg, uint16_t endCapBg) { if (a1 <= a0) return; - if (drawStartCap) drawArcCapAA(tft, cx, cy, radius, innerRadius, a0, fg_color, startCapBg); - if (drawEndCap) drawArcCapAA(tft, cx, cy, radius, innerRadius, a1, fg_color, endCapBg); - drawArcAA(tft, cx, cy, radius, innerRadius, a0, a1, fg_color, bg_color); + if (drawStartCap) drawArcCapAA(gfx, cx, cy, radius, innerRadius, a0, fg_color, startCapBg); + if (drawEndCap) drawArcCapAA(gfx, cx, cy, radius, innerRadius, a1, fg_color, endCapBg); + drawArcAA(gfx, cx, cy, radius, innerRadius, a0, a1, fg_color, bg_color); } -static void drawArcFill(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, +static void drawArcFill(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, int16_t thickness, uint16_t fillEnd, uint16_t fillColor, bool forceRedraw) { const uint16_t startAngle = 60; @@ -407,19 +407,19 @@ static void drawArcFill(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, const int16_t innerRadius = radius - thickness; if (forceRedraw) { - tft.fillCircle(cx, cy, radius + 2, bg); - drawArcSegmentAA(tft, cx, cy, radius, innerRadius, + gfx.fillCircle(cx, cy, radius + 2, bg); + drawArcSegmentAA(gfx, cx, cy, radius, innerRadius, startAngle, endAngle, track, bg, true, true, bg, bg); } if (clampedFillEnd > startAngle) { - drawArcSegmentAA(tft, cx, cy, radius, innerRadius, + drawArcSegmentAA(gfx, cx, cy, radius, innerRadius, startAngle, clampedFillEnd, fillColor, bg, true, true, bg, (clampedFillEnd < endAngle) ? track : bg); } if (clampedFillEnd < endAngle) { - drawArcSegmentAA(tft, cx, cy, radius, innerRadius, + drawArcSegmentAA(gfx, cx, cy, radius, innerRadius, clampedFillEnd, endAngle, track, bg, false, true, bg, bg); } @@ -428,10 +428,10 @@ static void drawArcFill(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, // --------------------------------------------------------------------------- // Helper: clear gauge center and prepare for text // --------------------------------------------------------------------------- -static void clearGaugeCenter(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, +static void clearGaugeCenter(lgfx::LovyanGFX& gfx, 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); + gfx.fillCircle(cx, cy, textR, dispSettings.bgColor); } // --------------------------------------------------------------------------- @@ -493,10 +493,10 @@ void resetGaugeTextCache() { // --------------------------------------------------------------------------- // Main progress arc // --------------------------------------------------------------------------- -void drawProgressArc(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawProgressArc(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, int16_t thickness, uint8_t progress, uint8_t prevProgress, uint16_t remainingMin, bool forceRedraw) { - ScopedWrite sw(tft); + ScopedWrite sw(gfx); const uint16_t startAngle = 60; const GaugeColors& gc = dispSettings.progress; uint16_t bg = dispSettings.bgColor; @@ -504,7 +504,7 @@ void drawProgressArc(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radiu uint16_t fillEnd = startAngle + (progress * 240) / 100; if (fillEnd > 300) fillEnd = 300; - drawArcFill(tft, cx, cy, radius, thickness, fillEnd, gc.arc, forceRedraw); + drawArcFill(gfx, cx, cy, radius, thickness, fillEnd, gc.arc, forceRedraw); bool compact = (radius < 50); @@ -524,22 +524,22 @@ void drawProgressArc(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radiu // Only clear center + redraw text when displayed string actually changes if (gaugeTextChanged(cx, cy, pctBuf, timeBuf, forceRedraw)) { - clearGaugeCenter(tft, cx, cy, radius, thickness); + clearGaugeCenter(gfx, cx, cy, radius, thickness); - tft.setTextDatum(MC_DATUM); - tft.setTextColor(gc.value); - tft.setTextFont(4); - tft.drawString(pctBuf, cx, cy - (compact ? 4 : 8)); + gfx.setTextDatum(MC_DATUM); + gfx.setTextColor(gc.value); + gfx.setTextFont(4); + gfx.drawString(pctBuf, cx, cy - (compact ? 4 : 8)); - tft.setTextFont(compact ? 1 : 2); - tft.setTextColor(CLR_TEXT_DIM); - tft.drawString(timeBuf, cx, cy + (compact ? 10 : 18)); + gfx.setTextFont(compact ? 1 : 2); + gfx.setTextColor(CLR_TEXT_DIM); + gfx.drawString(timeBuf, cx, cy + (compact ? 10 : 18)); if (compact) { bool sm = dispSettings.smallLabels; - tft.setTextFont(sm ? 1 : 2); - tft.setTextColor(gc.label, bg); - tft.drawString("Progress", cx, cy + radius + (sm ? 3 : -1)); + gfx.setTextFont(sm ? 1 : 2); + gfx.setTextColor(gc.label, bg); + gfx.drawString("Progress", cx, cy + radius + (sm ? 3 : -1)); } } } @@ -547,12 +547,12 @@ void drawProgressArc(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radiu // --------------------------------------------------------------------------- // Temperature arc gauge // --------------------------------------------------------------------------- -void drawTempGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawTempGauge(lgfx::LovyanGFX& gfx, 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, const GaugeColors* colors, float arcValue) { - ScopedWrite sw(tft); + ScopedWrite sw(gfx); const uint16_t startAngle = 60; const int16_t thickness = 6; uint16_t bg = dispSettings.bgColor; @@ -576,7 +576,7 @@ void drawTempGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, uint16_t tempColor = arcColor; uint16_t drawFill = (ratio > 0.01f) ? fillEnd : startAngle; - drawArcFill(tft, cx, cy, radius, thickness, drawFill, tempColor, forceRedraw); + drawArcFill(gfx, cx, cy, radius, thickness, drawFill, tempColor, forceRedraw); // Build display strings char tempBuf[12], targetBuf[12]; @@ -587,34 +587,34 @@ void drawTempGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, // Only clear center + redraw text when displayed string actually changes if (gaugeTextChanged(cx, cy, tempBuf, targetBuf, forceRedraw)) { - clearGaugeCenter(tft, cx, cy, radius, thickness); + clearGaugeCenter(gfx, cx, cy, radius, thickness); - tft.setTextDatum(MC_DATUM); - tft.setTextFont(4); - tft.setTextColor(valColor); - tft.drawString(tempBuf, cx, hasTarget ? (cy - 4) : cy); + gfx.setTextDatum(MC_DATUM); + gfx.setTextFont(4); + gfx.setTextColor(valColor); + gfx.drawString(tempBuf, cx, hasTarget ? (cy - 4) : cy); if (hasTarget) { - tft.setTextFont(1); - tft.setTextColor(CLR_TEXT_DIM); - tft.drawString(targetBuf, cx, cy + 10); + gfx.setTextFont(1); + gfx.setTextColor(CLR_TEXT_DIM); + gfx.drawString(targetBuf, cx, cy + 10); } bool sm = dispSettings.smallLabels; - tft.setTextFont(sm ? 1 : 2); - tft.setTextColor(lblColor, bg); - tft.drawString(label, cx, cy + radius + (sm ? 3 : -1)); + gfx.setTextFont(sm ? 1 : 2); + gfx.setTextColor(lblColor, bg); + gfx.drawString(label, cx, cy + radius + (sm ? 3 : -1)); } } // --------------------------------------------------------------------------- // Fan speed gauge (0-100%) // --------------------------------------------------------------------------- -void drawFanGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawFanGauge(lgfx::LovyanGFX& gfx, 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) { - ScopedWrite sw(tft); + ScopedWrite sw(gfx); const uint16_t startAngle = 60; const int16_t thickness = 6; uint16_t bg = dispSettings.bgColor; @@ -636,7 +636,7 @@ void drawFanGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, } uint16_t drawFill = (arcVal > 0.5f) ? fillEnd : startAngle; - drawArcFill(tft, cx, cy, radius, thickness, drawFill, fanColor, forceRedraw); + drawArcFill(gfx, cx, cy, radius, thickness, drawFill, fanColor, forceRedraw); // Build display string char buf[8]; @@ -644,27 +644,27 @@ void drawFanGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, // Only clear center + redraw text when displayed value actually changes if (gaugeTextChanged(cx, cy, buf, "", forceRedraw)) { - clearGaugeCenter(tft, cx, cy, radius, thickness); + clearGaugeCenter(gfx, cx, cy, radius, thickness); - tft.setTextDatum(MC_DATUM); - tft.setTextFont(4); - tft.setTextColor(valColor); - tft.drawString(buf, cx, cy); + gfx.setTextDatum(MC_DATUM); + gfx.setTextFont(4); + gfx.setTextColor(valColor); + gfx.drawString(buf, cx, cy); bool sm = dispSettings.smallLabels; - tft.setTextFont(sm ? 1 : 2); - tft.setTextColor(lblColor, bg); - tft.drawString(label, cx, cy + radius + (sm ? 3 : -1)); + gfx.setTextFont(sm ? 1 : 2); + gfx.setTextColor(lblColor, bg); + gfx.drawString(label, cx, cy + radius + (sm ? 3 : -1)); } } // --------------------------------------------------------------------------- // AMS humidity gauge (percentage from humidityRaw, color from humidity level) // --------------------------------------------------------------------------- -void drawHumidityGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawHumidityGauge(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, uint8_t humidityRaw, uint8_t humidityLevel, bool present, const char* label, bool forceRedraw) { - ScopedWrite sw(tft); + ScopedWrite sw(gfx); const uint16_t startAngle = 60; const int16_t thickness = 6; uint16_t bg = dispSettings.bgColor; @@ -688,7 +688,7 @@ void drawHumidityGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t rad } uint16_t drawFill = (pct > 0) ? fillEnd : startAngle; - drawArcFill(tft, cx, cy, radius, thickness, drawFill, arcColor, forceRedraw); + drawArcFill(gfx, cx, cy, radius, thickness, drawFill, arcColor, forceRedraw); // Build display string char buf[8]; @@ -699,27 +699,27 @@ void drawHumidityGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t rad } if (gaugeTextChanged(cx, cy, buf, "", forceRedraw)) { - clearGaugeCenter(tft, cx, cy, radius, thickness); + clearGaugeCenter(gfx, cx, cy, radius, thickness); - tft.setTextDatum(MC_DATUM); - tft.setTextFont(4); - tft.setTextColor(present ? CLR_TEXT : CLR_TEXT_DIM); - tft.drawString(buf, cx, cy); + gfx.setTextDatum(MC_DATUM); + gfx.setTextFont(4); + gfx.setTextColor(present ? CLR_TEXT : CLR_TEXT_DIM); + gfx.drawString(buf, cx, cy); bool sm = dispSettings.smallLabels; - tft.setTextFont(sm ? 1 : 2); - tft.setTextColor(arcColor, bg); - tft.drawString(label, cx, cy + radius + (sm ? 3 : -1)); + gfx.setTextFont(sm ? 1 : 2); + gfx.setTextColor(arcColor, bg); + gfx.drawString(label, cx, cy + radius + (sm ? 3 : -1)); } } // --------------------------------------------------------------------------- // Layer progress gauge (current / total) // --------------------------------------------------------------------------- -void drawLayerGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawLayerGauge(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, int16_t thickness, uint16_t layerNum, uint16_t totalLayers, bool forceRedraw) { - ScopedWrite sw(tft); + ScopedWrite sw(gfx); const uint16_t startAngle = 60; uint16_t bg = dispSettings.bgColor; uint16_t arcColor = dispSettings.progress.arc; @@ -731,7 +731,7 @@ void drawLayerGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius if (fillEnd > 300) fillEnd = 300; uint16_t drawFill = (ratio > 0.01f) ? fillEnd : startAngle; - drawArcFill(tft, cx, cy, radius, thickness, drawFill, arcColor, forceRedraw); + drawArcFill(gfx, cx, cy, radius, thickness, drawFill, arcColor, forceRedraw); // Build display strings - use smaller font for large numbers char layerBuf[12], totalBuf[12]; @@ -743,42 +743,42 @@ void drawLayerGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius } if (gaugeTextChanged(cx, cy, layerBuf, totalBuf, forceRedraw)) { - clearGaugeCenter(tft, cx, cy, radius, thickness); + clearGaugeCenter(gfx, cx, cy, radius, thickness); - tft.setTextDatum(MC_DATUM); + gfx.setTextDatum(MC_DATUM); // Pick font size based on digit count to fit inside gauge bool hasTot = (totalLayers > 0); int digits = strlen(layerBuf) + strlen(totalBuf); bool useSmall = (digits > 7); - tft.setTextFont(useSmall ? 2 : 4); - tft.setTextColor(CLR_TEXT); - tft.drawString(layerBuf, cx, hasTot ? (cy - 4) : cy); + gfx.setTextFont(useSmall ? 2 : 4); + gfx.setTextColor(CLR_TEXT); + gfx.drawString(layerBuf, cx, hasTot ? (cy - 4) : cy); if (hasTot) { - tft.setTextFont(useSmall ? 1 : 2); - tft.setTextColor(CLR_TEXT_DIM); - tft.drawString(totalBuf, cx, cy + (useSmall ? 8 : 10)); + gfx.setTextFont(useSmall ? 1 : 2); + gfx.setTextColor(CLR_TEXT_DIM); + gfx.drawString(totalBuf, cx, cy + (useSmall ? 8 : 10)); } bool sm = dispSettings.smallLabels; - tft.setTextFont(sm ? 1 : 2); - tft.setTextColor(arcColor, bg); - tft.drawString("Layer", cx, cy + radius + (sm ? 3 : -1)); + gfx.setTextFont(sm ? 1 : 2); + gfx.setTextColor(arcColor, bg); + gfx.drawString("Layer", cx, cy + radius + (sm ? 3 : -1)); } } // --------------------------------------------------------------------------- // Clock widget - shows current time HH:MM inside a track ring // --------------------------------------------------------------------------- -void drawClockWidget(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawClockWidget(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, int16_t thickness, bool forceRedraw) { - ScopedWrite sw(tft); + ScopedWrite sw(gfx); uint16_t bg = dispSettings.bgColor; if (forceRedraw) { - tft.fillCircle(cx, cy, radius + 2, bg); + gfx.fillCircle(cx, cy, radius + 2, bg); } // Get current time @@ -800,16 +800,16 @@ void drawClockWidget(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radiu } if (gaugeTextChanged(cx, cy, timeBuf, "", forceRedraw)) { - tft.fillCircle(cx, cy, radius - 1, bg); + gfx.fillCircle(cx, cy, radius - 1, bg); - tft.setTextDatum(MC_DATUM); - tft.setTextFont(4); - tft.setTextColor(CLR_TEXT); - tft.drawString(timeBuf, cx, cy); + gfx.setTextDatum(MC_DATUM); + gfx.setTextFont(4); + gfx.setTextColor(CLR_TEXT); + gfx.drawString(timeBuf, cx, cy); bool sm = dispSettings.smallLabels; - tft.setTextFont(sm ? 1 : 2); - tft.setTextColor(CLR_TEXT_DIM, bg); - tft.drawString("Clock", cx, cy + radius + (sm ? 3 : -1)); + gfx.setTextFont(sm ? 1 : 2); + gfx.setTextColor(CLR_TEXT_DIM, bg); + gfx.drawString("Clock", cx, cy + radius + (sm ? 3 : -1)); } } diff --git a/src/display_gauges.h b/src/display_gauges.h index 454e791..a5ee311 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::LovyanGFX& tft, int16_t y, uint8_t progress); +void drawLedProgressBar(lgfx::LovyanGFX& gfx, int16_t y, uint8_t progress); // Shimmer animation tick — call from loop(), runs at its own cadence -void tickProgressShimmer(lgfx::LovyanGFX& tft, int16_t y, uint8_t progress, bool printing); +void tickProgressShimmer(lgfx::LovyanGFX& gfx, int16_t y, uint8_t progress, bool printing); // Draw progress arc with percentage and time in center -void drawProgressArc(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawProgressArc(lgfx::LovyanGFX& gfx, 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::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawTempGauge(lgfx::LovyanGFX& gfx, 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,22 +27,22 @@ void drawTempGauge(lgfx::LovyanGFX& 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(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawFanGauge(lgfx::LovyanGFX& gfx, 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); // Draw clock widget (HH:MM inside track ring) -void drawClockWidget(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawClockWidget(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, int16_t thickness, bool forceRedraw); // Draw AMS humidity gauge (humidityRaw % with color from humidity level) -void drawHumidityGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawHumidityGauge(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, uint8_t humidityRaw, uint8_t humidityLevel, bool present, const char* label, bool forceRedraw); // Draw layer progress gauge (current / total layers) -void drawLayerGauge(lgfx::LovyanGFX& tft, int16_t cx, int16_t cy, int16_t radius, +void drawLayerGauge(lgfx::LovyanGFX& gfx, int16_t cx, int16_t cy, int16_t radius, int16_t thickness, uint16_t layerNum, uint16_t totalLayers, bool forceRedraw); diff --git a/src/display_ui.cpp b/src/display_ui.cpp index 972da39..4b77ec0 100644 --- a/src/display_ui.cpp +++ b/src/display_ui.cpp @@ -203,6 +203,29 @@ class LGFX_WS154 : public lgfx::LGFX_Device { }; static LGFX_WS154 _tft_instance; +#elif defined(BOARD_IS_JC3248W535) +// --- Guition JC3248W535 + AXS15231B 320x480 --------------------------------- +// Panel_AXS15231B_AGFX wraps moononournation/Arduino_GFX's Arduino_AXS15231B +// driver inside a LovyanGFX Panel_Device subclass. Mainline LovyanGFX has +// neither an AXS15231B panel class nor a QSPI bus, and a hand-rolled custom +// driver didn't produce correct pixels on this hardware. Arduino_GFX does — +// this wrapper lets the whole codebase keep calling the LovyanGFX API on +// `tft` while the physical QSPI traffic is handled by Arduino_GFX. +// Backlight is a simple GPIO-high (LEDC PWM not required for on/off). +#include "lgfx_panel_axs15231b_agfx.hpp" +class LGFX_JC3248W535 : public lgfx::LGFX_Device { + lgfx::Panel_AXS15231B_AGFX _panel; +public: + LGFX_JC3248W535() { + // Panel_AXS15231B_AGFX owns the Arduino_GFX bus+panel internally. Pins + // are hard-coded in its constructor to the verified JC3248W535 map + // (CS=45, SCK=47, D0=21, D1=48, D2=40, D3=39) since Arduino_GFX's + // databus class hard-codes them at construction anyway. + setPanel(&_panel); + } + lgfx::Panel_AXS15231B_AGFX* panelAXS() { return &_panel; } +}; +static LGFX_JC3248W535 _tft_instance; #elif defined(BOARD_IS_C3) // --- ESP32-C3 Super Mini + ST7789 240x280 ------------------------------------ class LGFX_C3 : public lgfx::LGFX_Device { @@ -244,7 +267,7 @@ class LGFX_C3 : public lgfx::LGFX_Device { static LGFX_C3 _tft_instance; #else - #error "No board variant defined. Add BOARD_IS_S3, DISPLAY_CYD, BOARD_IS_C3, BOARD_IS_WS200 or BOARD_IS_WS154 to build_flags." + #error "No board variant defined. Add BOARD_IS_S3, DISPLAY_CYD, BOARD_IS_C3, BOARD_IS_WS200, BOARD_IS_WS154 or BOARD_IS_JC3248W535 to build_flags." #endif // Global pointer + reference — accessed via `tft` throughout the codebase. @@ -252,7 +275,49 @@ static LGFX_C3 _tft_instance; // populated with either the V2 or Classic panel in initDisplay(), so method // calls via this reference/pointer dispatch to whichever variant was chosen. lgfx::LovyanGFX* tft_ptr = &_tft_instance; -lgfx::LovyanGFX& tft = *tft_ptr; +// `tft` is now a macro in display_ui.h — `#define tft (*tft_ptr)` — so +// every call site re-dereferences the pointer and picks up runtime +// retargeting to the JC3248W535 PSRAM sprite. + +// Direct panel pointer for JC3248W535 sprite escape-hatch; nullptr on all +// other boards so the extern declaration in display_ui.h is always satisfied. +#if defined(BOARD_IS_JC3248W535) +lgfx::Panel_AXS15231B_AGFX* g_axs_panel = _tft_instance.panelAXS(); + +// Full-frame PSRAM sprite. All BambuHelper draws are redirected here in +// initDisplay() (via tft_ptr), then flushed to the panel once per loop() +// tick via flushFrame(). The AXS15231B in QSPI mode cannot address +// arbitrary Y per draw (see lgfx_panel_axs15231b_agfx.hpp), so a +// framebuffer-and-single-raster-flush is the only reliable render path. +static lgfx::LGFX_Sprite _frame_sprite(&_tft_instance); +#else +lgfx::Panel_AXS15231B_AGFX* g_axs_panel = nullptr; +#endif + +void flushFrame() { +#if defined(BOARD_IS_JC3248W535) + if (g_axs_panel && _frame_sprite.getBuffer()) { + g_axs_panel->pushRawPixels( + static_cast(_frame_sprite.getBuffer()), + 320u * 480u); + } +#endif +} + +// JC3248W535 currently only supports portrait rotations (0 and 2) in the +// sprite-push architecture — layout_320x480.h is hard-coded portrait and no +// landscape layout exists. Snap odd rotations to 0 with a Serial warning. +static uint8_t sanitizeRotation(uint8_t r) { +#if defined(BOARD_IS_JC3248W535) + if (r == 1 || r == 3) { + Serial.printf("Display: rotation %u unsupported on JC3248W535 " + "(landscape layout not yet available); snapping to 0\n", + (unsigned)r); + return 0; + } +#endif + return r; +} // Use user-configured bg color instead of hardcoded CLR_BG #undef CLR_BG @@ -374,7 +439,14 @@ void initDisplay() { tft.setRotation(0); tft.fillScreen(TFT_BLACK); #endif +#if defined(BOARD_IS_JC3248W535) + // Panel MADCTL stays at 0 forever — RASET-skip + LSB-first byte-order + // invariants in pushRawPixels depend on native orientation. User-facing + // rotation is applied to the PSRAM sprite after tft_ptr is redirected. + tft.setRotation(0); +#else tft.setRotation(dispSettings.rotation); +#endif #if defined(DISPLAY_CYD) applyCydPanelInversion(); #elif defined(DISPLAY_240x320) @@ -384,6 +456,27 @@ void initDisplay() { tft.fillScreen(CLR_BG); Serial.println("Display: fillScreen done"); +#if defined(BOARD_IS_JC3248W535) + // Allocate 320x480x16bpp PSRAM sprite (300 KB) and redirect tft_ptr so all + // subsequent draws (splash, UI, refreshes) render into the sprite buffer. + // Panel cannot address arbitrary Y in QSPI mode — instead we flush the + // whole sprite to the panel once per loop tick via flushFrame(). + _frame_sprite.setPsram(true); + _frame_sprite.setColorDepth(16); + if (_frame_sprite.createSprite(320, 480)) { + _frame_sprite.setTextDatum(MC_DATUM); // match the tft defaults used below + tft_ptr = &_frame_sprite; + Serial.printf("Display: frame sprite 320x480 allocated in PSRAM, free=%u\n", + (unsigned)heap_caps_get_free_size(MALLOC_CAP_SPIRAM)); + tft.setRotation(sanitizeRotation(dispSettings.rotation)); + tft.fillScreen(CLR_BG); + flushFrame(); // push cleared sprite so panel shows CLR_BG during splash + } else { + Serial.println("Display: frame sprite alloc FAILED — will draw direct to panel (expect artifacts)"); + } +#endif + + #if defined(TOUCH_CS) && !defined(USE_XPT2046) // LovyanGFX touch calibration uint16_t calData[8] = {0, 0, 0, 65535, 0, 65535, 65535, 65535}; @@ -416,7 +509,7 @@ void applyDisplaySettings() { tft.setRotation(0); tft.fillScreen(TFT_BLACK); #endif - tft.setRotation(dispSettings.rotation); + tft.setRotation(sanitizeRotation(dispSettings.rotation)); #if defined(DISPLAY_CYD) applyCydPanelInversion(); #elif defined(DISPLAY_240x320) diff --git a/src/display_ui.h b/src/display_ui.h index 5df51d2..1d02d64 100644 --- a/src/display_ui.h +++ b/src/display_ui.h @@ -3,6 +3,10 @@ #include +// Forward-declare the panel type so callers can use the pointer without +// pulling in the full header (which includes Arduino_GFX headers). +namespace lgfx { inline namespace v1 { class Panel_AXS15231B_AGFX; } } + enum ScreenState { SCREEN_SPLASH, SCREEN_AP_MODE, @@ -18,11 +22,26 @@ enum ScreenState { }; extern lgfx::LovyanGFX* tft_ptr; -// Convenience reference — all callers use `tft.method()` unchanged. -extern lgfx::LovyanGFX& tft; +// Macro (NOT a reference) so callers' `tft.method()` always dereferences the +// current value of `tft_ptr`. On JC3248W535 we retarget this pointer to a +// PSRAM sprite at runtime; a C++ reference would have been permanently +// bound to the panel at static-init time, defeating the redirection. +#define tft (*tft_ptr) + +// Direct pointer to the AXS15231B panel wrapper; only non-null on +// BOARD_IS_JC3248W535 builds. Used by the sprite direct-push diagnostic. +extern lgfx::Panel_AXS15231B_AGFX* g_axs_panel; void initDisplay(); void updateDisplay(); + +// Flush the off-screen framebuffer sprite to the panel in one contiguous +// raster write. No-op on boards that draw directly (all except +// BOARD_IS_JC3248W535, which uses a full-screen PSRAM sprite to work around +// the AXS15231B QSPI-mode addressing limits). Call once per loop tick after +// UI draws to commit the frame. +void flushFrame(); + void setScreenState(ScreenState state); ScreenState getScreenState(); void setBacklight(uint8_t level); diff --git a/src/icons.h b/src/icons.h index 396aa21..d634d9c 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::LovyanGFX& tft, int16_t x, int16_t y, +inline void drawIcon16(lgfx::LovyanGFX& gfx, 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]); @@ -273,14 +273,14 @@ inline void drawIcon16(lgfx::LovyanGFX& tft, int16_t x, int16_t y, uint16_t bits = (b0 << 8) | b1; for (int col = 0; col < 16; col++) { if (bits & (0x8000 >> col)) { - tft.drawPixel(x + col, y + row, color); + gfx.drawPixel(x + col, y + row, color); } } } } // Helper: draw a 32x32 1-bit icon at (x, y) with given color, transparent bg -inline void drawIcon32(lgfx::LovyanGFX& tft, int16_t x, int16_t y, +inline void drawIcon32(lgfx::LovyanGFX& gfx, 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) | @@ -289,7 +289,7 @@ inline void drawIcon32(lgfx::LovyanGFX& tft, int16_t x, int16_t y, (uint32_t)pgm_read_byte(&icon[row * 4 + 3]); for (int col = 0; col < 32; col++) { if (bits & (0x80000000UL >> col)) { - tft.drawPixel(x + col, y + row, color); + gfx.drawPixel(x + col, y + row, color); } } } diff --git a/src/lgfx_panel_axs15231b_agfx.hpp b/src/lgfx_panel_axs15231b_agfx.hpp new file mode 100644 index 0000000..b3b3cf2 --- /dev/null +++ b/src/lgfx_panel_axs15231b_agfx.hpp @@ -0,0 +1,303 @@ +// LovyanGFX Panel_Device subclass that owns an internal Arduino_GFX instance +// (Arduino_ESP32QSPI + Arduino_AXS15231B) and forwards LovyanGFX's low-level +// drawing primitives to it. Lets the whole of BambuHelper keep calling +// `tft.fillRect()`, `tft.drawString()`, etc. on a LovyanGFX reference without +// changing any call sites, while the actual QSPI traffic is handled by the +// proven-working moononournation/Arduino_GFX driver. +// +// Why this exists: mainline LovyanGFX has no AXS15231B panel class. A +// hand-rolled one never produced correct pixels on this hardware, and +// Arduino_GFX's Arduino_AXS15231B drives the chip correctly out of the box. +// Wrapping it inside a LovyanGFX Panel subclass keeps the rest of the app +// on a single graphics API. + +#pragma once + +#include +#include + +// --------------------------------------------------------------------------- +// Arduino_AXS15231B_QSPI — subclass of Arduino_AXS15231B that fixes two +// QSPI-mode quirks the stock driver gets wrong (both documented in the +// jc3248w535 skill's failure-mode table): +// +// 1. RASET (0x2B) must NOT be sent in QSPI mode. The panel derives rows +// from the RAMWR/RAMWRC pixel stream itself. Sending RASET after CASET +// causes "Only one corner displays, rest is garbage" — every draw lands +// at the chip's RAM origin regardless of the requested (x,y). Confirmed +// against the manufacturer's esp_lcd_axs15231b.c which only sends +// RASET when `flags.use_qspi_interface == 0`. +// +// 2. COLMOD (0x3A) is not set by Arduino_GFX's init table. On some batches +// the POR default is RGB666 (3-byte pixels), which misaligns against +// our 16-bit RGB565 DMA output and produces a stripe/rainbow pattern. +// Send 0x05 (RGB565, 16bpp) once after begin() to lock the format. +// --------------------------------------------------------------------------- +class Arduino_AXS15231B_QSPI : public Arduino_AXS15231B { +public: + using Arduino_AXS15231B::Arduino_AXS15231B; + + bool begin(int32_t speed = GFX_NOT_DEFINED) override { + if (!Arduino_AXS15231B::begin(speed)) return false; + // Force RGB565 / 16bpp COLMOD. Done after the base init sequence so it + // overrides any POR default and any drift during init. + _bus->beginWrite(); + _bus->writeC8D8(0x3A /*COLMOD*/, 0x05 /*RGB565 16bpp, AXS15231B encoding*/); + _bus->endWrite(); + return true; + } + + void writeAddrWindow(int16_t x, int16_t y, uint16_t w, + uint16_t h) override { + if ((x != _currentX) || (w != _currentW)) { + _currentX = x; + _currentW = w; + x += _xStart; + _bus->writeC8D16D16(AXS15231B_CASET, x, x + w - 1); + } + // RASET intentionally skipped — QSPI mode derives y from pixel stream. + // Cache y/h for consistency with parent expectations, but never send + // 0x2B. The panel uses RAMWR (0x2C) in writeCommand below to reset the + // write pointer to the top of the CASET window. + _currentY = y; + _currentH = h; + _bus->writeCommand(AXS15231B_RAMWR); + } +}; + +namespace lgfx { +inline namespace v1 { + +class Panel_AXS15231B_AGFX : public Panel_Device { +public: + Panel_AXS15231B_AGFX() { + _cfg.memory_width = _cfg.panel_width = 320; + _cfg.memory_height = _cfg.panel_height = 480; + _cfg.offset_x = 0; + _cfg.offset_y = 0; + _cfg.offset_rotation = 0; + _cfg.dummy_read_pixel = 0; + _cfg.dummy_read_bits = 0; + _cfg.readable = false; + _cfg.invert = false; + _cfg.rgb_order = false; + _cfg.dlen_16bit = false; + _cfg.bus_shared = false; + _write_depth = color_depth_t::rgb565_2Byte; + _read_depth = color_depth_t::rgb565_2Byte; + } + + ~Panel_AXS15231B_AGFX() { + if (_agfx) { delete _agfx; _agfx = nullptr; } + if (_agfx_bus) { delete _agfx_bus; _agfx_bus = nullptr; } + } + + // ------------------------------------------------------------------------- + // Init / lifecycle + // ------------------------------------------------------------------------- + + bool init(bool /*use_reset*/) override { + if (_init_done) return true; + _agfx_bus = new Arduino_ESP32QSPI( + 45 /*CS*/, 47 /*SCK*/, 21 /*D0*/, 48 /*D1*/, 40 /*D2*/, 39 /*D3*/); + _agfx = new Arduino_AXS15231B_QSPI( + _agfx_bus, + -1 /*RST, software-reset only*/, + 0 /*rotation*/, + false /*IPS — MUST be false to avoid double-inversion*/, + 320, 480); + if (!_agfx->begin(32000000UL)) { + delete _agfx; _agfx = nullptr; + delete _agfx_bus; _agfx_bus = nullptr; + return false; + } + // IMPORTANT: do NOT call _agfx->fillScreen() here. Arduino_GFX's begin() + // ends with setAddrWindow(0,0,w,h) which caches _currentX/Y/W/H. A + // fillScreen at this point would skip the CASET/RASET re-send because + // state already matches — and the AXS15231B appears to need those + // explicitly re-sent after the long init sequence, or only a small sliver + // at the chip's RAM origin paints. LovyanGFX's post-init setRotation(0) + // call invalidates the Arduino_TFT _current* cache via Arduino_TFT:: + // setRotation (sets to 0xFFFF), which forces the next writeAddrWindow to + // re-send CASET and RASET. After that, fills work correctly. See the + // Arduino_TFT.cpp:137-177 for the cache logic. + _init_done = true; + _width = _cfg.panel_width; + _height = _cfg.panel_height; + return true; + } + + // Arduino_GFX owns its bus entirely, so LovyanGFX's bus plumbing is unused. + void initBus(void) override {} + void releaseBus(void) override {} + + void beginTransaction(void) override { + if (_agfx && !_in_transaction) { + _agfx->startWrite(); + _in_transaction = true; + } + } + + void endTransaction(void) override { + if (_agfx && _in_transaction) { + _agfx->endWrite(); + _in_transaction = false; + } + } + + color_depth_t setColorDepth(color_depth_t) override { + _write_depth = color_depth_t::rgb565_2Byte; + _read_depth = color_depth_t::rgb565_2Byte; + return _write_depth; + } + + void setRotation(uint_fast8_t r) override { + r &= 3; + _rotation = r; + _internal_rotation = r; + if (_agfx) _agfx->setRotation(r); + _width = (r & 1) ? _cfg.panel_height : _cfg.panel_width; + _height = (r & 1) ? _cfg.panel_width : _cfg.panel_height; + } + + void setInvert(bool /*invert*/) override {} + void setSleep(bool /*flg*/) override {} + void setPowerSave(bool /*flg*/) override {} + void waitDisplay(void) override {} + bool displayBusy(void) override { return false; } + + // Panel_Device's defaults for these four all dereference `_bus` (inherited + // from IPanel, nullptr in this wrapper because Arduino_GFX owns its own + // bus). Override to no-ops — Arduino_GFX flushes each draw immediately so + // there's no deferred DMA state to wait on. + void initDMA(void) override {} + void waitDMA(void) override {} + bool dmaBusy(void) override { return false; } + void display(uint_fast16_t, uint_fast16_t, uint_fast16_t, uint_fast16_t) override {} + + // ------------------------------------------------------------------------- + // Drawing primitives — LovyanGFX calls these after clipping. Everything + // higher-level (fillScreen, drawString, pushImage) funnels through here. + // ------------------------------------------------------------------------- + + void setWindow(uint_fast16_t xs, uint_fast16_t ys, + uint_fast16_t xe, uint_fast16_t ye) override { + if (!_agfx) return; + // writeAddrWindow is transaction-internal (uses the bus mid-write). + // LovyanGFX always wraps setWindow in beginTransaction/endTransaction, + // so the bus is already open when we get here. + _agfx->writeAddrWindow(xs, ys, (xe - xs + 1), (ye - ys + 1)); + } + + void drawPixelPreclipped(uint_fast16_t x, uint_fast16_t y, + uint32_t rawcolor) override { + if (!_agfx) return; + // Arduino_GFX's write*Preclipped variants assume we're inside an open + // startWrite/endWrite transaction — which we always are when LovyanGFX + // calls these, because LGFXBase wraps draws in beginTransaction which + // we map to _agfx->startWrite(). + _agfx->writePixelPreclipped(x, y, (uint16_t)rawcolor); + } + + void writeFillRectPreclipped(uint_fast16_t x, uint_fast16_t y, + uint_fast16_t w, uint_fast16_t h, + uint32_t rawcolor) override { + if (!_agfx) return; + _agfx->writeFillRectPreclipped(x, y, w, h, (uint16_t)rawcolor); + } + + void writeBlock(uint32_t rawcolor, uint32_t length) override { + if (!_agfx || length == 0) return; + _agfx->writeRepeat((uint16_t)rawcolor, length); + } + + void writePixels(pixelcopy_t* pc, uint32_t length, bool /*use_dma*/) override { + if (!_agfx || length == 0) return; + // Use a large staging buffer so a full-screen pushSprite becomes a + // single Arduino_GFX writePixels call (one CS cycle, one RAMWRC header + // followed by VARIABLE-CMD continuation chunks with CS held low). + // 4096 pixels × 2 bytes = 8 KB static buf; no stack impact. + static constexpr uint32_t BUF_PIXELS = 4096; + static uint16_t buf[BUF_PIXELS]; + while (length > 0) { + uint32_t n = length > BUF_PIXELS ? BUF_PIXELS : length; + pc->fp_copy(buf, 0, n, pc); + _agfx->writePixels(buf, n); + length -= n; + } + } + + void writeImage(uint_fast16_t x, uint_fast16_t y, + uint_fast16_t w, uint_fast16_t h, + pixelcopy_t* pc, bool use_dma) override { + if (!_agfx || w == 0 || h == 0) return; + // Always inside an open transaction when LovyanGFX calls us. + _agfx->writeAddrWindow(x, y, w, h); + writePixels(pc, (uint32_t)w * h, use_dma); + } + + // ------------------------------------------------------------------------- + // Direct push escape-hatch: call _agfx->writePixels() with the whole + // contiguous pixel buffer in ONE call. Arduino_ESP32QSPI::writePixels + // then handles it as one CS cycle with a single RAMWRC header followed + // by internal VARIABLE-flag continuation chunks. This is the only way + // to push a large framebuffer without the chip getting confused by + // multiple RAMWRC commands from chunked calls. Intended for full-frame + // sprite flushes (e.g. pushing a PSRAM LGFX_Sprite as one atomic frame). + // ------------------------------------------------------------------------- + void pushRawPixels(uint16_t* data, uint32_t length) { + if (!_agfx || length == 0) return; + // Arduino_GFX's MSB_32_16_16_SET byte-swaps each pixel from native LE + // to big-endian MSB-first before DMA, which is the MIPI DCS convention + // for 16bpp pixel data. But this chip in QSPI mode evidently reads + // pixels LSB-first (observed: RED→BLUE, GREEN→RED, BLUE→GREEN, + // YELLOW→MAGENTA — exactly the pattern of byte-swapped RGB565). Pre- + // swap here to cancel Arduino_GFX's swap so the net wire byte order + // matches what the chip expects. Restore the sprite buffer afterwards + // so repeat pushes work. + for (uint32_t i = 0; i < length; ++i) { + data[i] = __builtin_bswap16(data[i]); + } + _agfx->startWrite(); + _agfx->writeAddrWindow(0, 0, _cfg.panel_width, _cfg.panel_height); + _agfx->writePixels(data, length); + _agfx->endWrite(); + for (uint32_t i = 0; i < length; ++i) { + data[i] = __builtin_bswap16(data[i]); + } + } + + // ------------------------------------------------------------------------- + // Read path — the panel is not readable in QSPI mode. Return zeros. + // ------------------------------------------------------------------------- + + uint32_t readCommand(uint_fast16_t, uint_fast8_t, uint_fast8_t) override { return 0; } + uint32_t readData(uint_fast8_t, uint_fast8_t) override { return 0; } + void readRect(uint_fast16_t, uint_fast16_t, uint_fast16_t, uint_fast16_t, + void*, pixelcopy_t*) override {} + + int32_t getScanLine(void) override { return 0; } + + // Command/data ops are unused — we don't own a separate bus. + void writeCommand(uint32_t, uint_fast8_t) override {} + void writeData(uint32_t, uint_fast8_t) override {} + + // Alpha-blend / copy paths — not exercised by BambuHelper, and the + // Panel_Device defaults touch `_bus`. Stub them to no-ops. + void writeImageARGB(uint_fast16_t, uint_fast16_t, + uint_fast16_t, uint_fast16_t, + pixelcopy_t*) override {} + void copyRect(uint_fast16_t, uint_fast16_t, + uint_fast16_t, uint_fast16_t, + uint_fast16_t, uint_fast16_t) override {} + +private: + bool _init_done = false; + bool _in_transaction = false; + // Renamed from `_bus` to avoid shadowing Panel_Device's protected IBus*_bus. + Arduino_DataBus* _agfx_bus = nullptr; + Arduino_TFT* _agfx = nullptr; // exposes writeAddrWindow/writeRepeat/writePixels +}; + +} // namespace v1 +} // namespace lgfx diff --git a/src/main.cpp b/src/main.cpp index 6cc125f..78832c4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -455,7 +455,10 @@ void setup() { } void loop() { - if (handleSplashPhase()) return; + if (handleSplashPhase()) { + flushFrame(); // commit splash draws to panel (no-op on non-JC boards) + return; + } handleWiFi(); handleWebServer(); @@ -480,4 +483,9 @@ void loop() { handleBambuMqtt(); handleRotation(); } + + // Commit the framebuffer sprite to the panel. On JC3248W535 this is a + // ~20ms QSPI push (300 KB @ 32MHz QIO); on all other boards it's a no-op + // since draws go directly to the panel. + flushFrame(); } diff --git a/src/settings.cpp b/src/settings.cpp index bb332c0..dc43d18 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -1,5 +1,6 @@ #include "settings.h" #include "config.h" +#include "button.h" #include "buzzer.h" #include "timezones.h" #include @@ -328,7 +329,7 @@ void loadSettings() { rotState.lastRotateMs = 0; // Button settings -#if defined(USE_CST816) || defined(USE_XPT2046) || defined(TOUCH_CS) +#if defined(USE_CST816) || defined(USE_XPT2046) || defined(USE_AXS_TOUCH) || defined(TOUCH_CS) buttonType = (ButtonType)prefs.getUChar("btn_type", BTN_TOUCHSCREEN); #else buttonType = (ButtonType)prefs.getUChar("btn_type", BTN_DISABLED); @@ -471,6 +472,7 @@ void saveRotationSettings() { } void saveButtonSettings() { + sanitizeButtonPin(); prefs.begin(NVS_NAMESPACE, false); prefs.putUChar("btn_type", buttonType); prefs.putUChar("btn_pin", buttonPin);