Skip to content

Add headless C3 build with virtual display over HTTP#1

Open
theNailz wants to merge 6 commits intofeature/lovyan-gfxfrom
feature/headless-c3
Open

Add headless C3 build with virtual display over HTTP#1
theNailz wants to merge 6 commits intofeature/lovyan-gfxfrom
feature/headless-c3

Conversation

@theNailz
Copy link
Copy Markdown
Owner

Summary

Adds an esp32c3-headless build environment for debugging display rendering on an ESP32-C3 without a physical screen. All drawing goes into an in-RAM LGFX_Sprite framebuffer, served as a live BMP image via HTTP.

  • New [env:esp32c3-headless] in platformio.ini with -D HEADLESS=1
  • In headless mode, tft points to an LGFX_Sprite instead of hardware — all draw calls captured
  • GET /screenshot streams a 240×280 BMP row-by-row (no large malloc, works on C3's limited SRAM)
  • GET /display serves a minimal HTML page with 2s auto-refresh — live virtual monitor in browser
  • Display function signatures widened from LGFX_Device& to LovyanGFX& (common base) so they accept both hardware and sprite targets
  • Hardware-specific calls (init(), invertDisplay()) use concrete _tft_instance directly
  • All 4 build environments pass: esp32s3, cyd, esp32c3, esp32c3-headless
  • Tested on device: C3 flashed and /display confirmed working in browser

What this is NOT

  • Not a production feature — debug/dev tool only
  • Does not affect any existing board or environment
  • Not related to double-buffering (separate issue)

Test plan

  • Build all 4 envs (esp32s3, cyd, esp32c3, esp32c3-headless)
  • Flash esp32c3-headless to C3 on COM6
  • Verify /screenshot returns valid 201KB BMP (200 OK)
  • Verify /display shows live virtual monitor in browser
  • OTA flash esp32s3 to S3 test device — confirm no regression

The ESP32 is too busy writing flash to respond to /ota/status poll
requests, so _autoOtaProgress often stays at a low value (e.g. 25%)
even when the download completes successfully. The previous >= 90%
threshold guard then blocked waitForReboot(), leaving the UI frozen.

Replace the progress threshold with an _otaInstallStarted flag set
after the server confirms the OTA task has begun. Any 404/network
error after install is confirmed now correctly triggers waitForReboot(),
regardless of the last polled progress value.
platformio.ini (esp32c3 env):
- Switch to partitions_4mb_c3.csv (1.875 MB OTA partitions)
- build_unflags: strip LOAD_GFXFF and SMOOTH_FONT — GFX free fonts and
  smooth VLW font renderer are compiled in by default but never used
- Explicit lib_deps without TFT_eWidget (dead code for C3)

partitions_4mb_c3.csv (new):
- app0/app1: 0x1E0000 (1.875 MB) each, up from 1.75 MB
- SPIFFS: 0x30000 (192 KB), reduced from 448 KB — SPIFFS unused in BambuHelper
- Result: 65 KB → 733 KB headroom
- Note: partition table change requires USB re-flash; OTA updates thereafter

src/main.cpp, src/display_ui.cpp:
- USB CDC startup delay and Serial.flush() removal (previously unstaged)
Replace TFT_eSPI with LovyanGFX across the entire display stack.
Board-specific configs (S3, CYD, C3) are C++ classes in display_ui.cpp.
SPI pins and driver settings move from build_flags into the class configs,
simplifying platformio.ini significantly. The C3 no longer needs the SPI
patch script.

Key changes:
- Three LGFX board classes: LGFX_S3 (ST7789), LGFX_CYD (ILI9342), LGFX_C3 (ST7789)
- fillArc replaces drawSmoothArc in gauge rendering; +90deg angle offset
  matches original gap-at-bottom orientation
- alphaBlend565() helper replaces tft.alphaBlend() (not a member in LGFX)
- drawArc (outline) replaces drawSmoothArc in animation code
- S3 requires invertDisplay(true) at runtime for correct colors
- Backlight handled via BACKLIGHT_PIN + analogWrite (unchanged)
- config.h: BACKLIGHT_PIN guard added (was hardcoded to TFT_BL)
New [env:esp32c3-headless] skips physical display init (tft.init,
backlight) and instead allocates a 240x280 LGFX_Sprite as a RAM
framebuffer. The full rendering pipeline runs unchanged against the
sprite.

Adds GET /screenshot (serves sprite as BMP) and GET /display (HTML
page with 2s auto-refresh) so display output can be observed in any
browser without a physical screen attached.

Useful for debugging display logic on a bare C3 module.
- Change tft type from LGFX_Device to LovyanGFX (common base) so it can
  point to either hardware display or LGFX_Sprite in headless mode
- In HEADLESS mode, tft now targets the sprite framebuffer so all draw
  calls are captured for the /screenshot endpoint
- Stream BMP row-by-row instead of malloc'ing the full 197KB image,
  fixing OOM crash on ESP32-C3 (no PSRAM)
- Update all helper functions (gauges, anims, icons) to accept
  LovyanGFX& instead of LGFX_Device&
- Sprite now uses LY_W×LY_H (240×240) instead of hardcoded 240×280,
  saving 19KB of SRAM on the C3
- This frees enough heap for the ~59KB settings page to load at /
- Use LY_W/LY_H in screenshot rowBuf and /display HTML dimensions
- Add layout.h include to web_server.cpp
@theNailz theNailz mentioned this pull request Apr 1, 2026
4 tasks
@theNailz theNailz force-pushed the feature/lovyan-gfx branch from ab274a7 to 645977b Compare April 17, 2026 07:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant