Hardware companion

The Lily build

Course 4's reaction–diffusion sim, running for real on a LilyGO T-QT Pro — a thumbnail-sized ESP32-S3 with a 0.85″ 128×128 color screen. Tune a pattern in the playground, export it, flash it, and it lives in your pocket. The two side buttons cycle presets and reseed.

What boots on the device — the Lily preset, running the exact 128×128 sim the firmware runs, one cell per pixel.
BoardESP32-S3FN4R2 · 4 MB flash · 2 MB PSRAM
Screen0.85″ GC9107 · 128×128 IPS
Footprint~282 KB RAM (86%) · ~277 KB flash
ToolchainPlatformIO · Arduino framework

Browser to board

The website and the firmware run the identical kernel — same 9-point Laplacian, same explicit Euler step — so a preset behaves the same in both. This is the whole pipeline:

Hardware

The display is driven with TFT_eSPI's GC9A01 driver (it speaks to the GC9107 correctly). All of this is configured in platformio.ini build flags — no library files are edited. Pin map verified against Bodmer's Setup211_LilyGo_T_QT_Pro_S3.h.

SignalGPIO
SPI MOSI2
SPI SCLK3
LCD CS5
LCD DC6
LCD RST1
Backlight10
Button — next preset0 (BOOT)
Button — reseed47

SPI runs at 40 MHz. MISO is unused (−1). Both buttons are active-low to ground.

Flash it

The cable question

The T-QT charges and flashes over its USB-C port — you need a USB-C data cable:

  • Works the white USB-C cable from a recent MacBook charger or an iPhone 15/16 (USB 2.0 data is plenty).
  • Catch a charge-only cable: the screen may light up but no serial port appears. If pio device list shows nothing, swap cables.
  • No an old iPhone Lightning cable won't fit; if your Mac is USB-A only, use a USB-A→USB-C data cable.

No driver needed on macOS — the ESP32-S3 uses native USB and shows up as a standard usbmodem port.

Test on your Mac first

Before flashing, run the pattern in a desktop window — it executes the exact same kernel the firmware does (shared gs_kernel.h), so what you see is byte-for-byte what the chip renders. No board, no chip emulation — the simulation is just portable math.

brew install sdl2      # one-time
make -C sim run        # opens the simulator window

Keys mirror the hardware buttons: N / → next preset · R reseed · Space pause · Q quit. This is how the 64→128 extinction bug got caught before a single flash.

Quick start

cd ~/Lily
pio device list        # confirm the board shows up
pio run -t upload      # build + flash
pio device monitor     # watch the log (Ctrl+C to quit)
Shell gotcha. Don't paste a command with a # comment after it — zsh passes the #… as arguments and PlatformIO errors with “Got unexpected extra arguments.” Type just the command.

If upload can't connect

Put the board into download mode by hand, then upload again:

  1. Hold BOOT (the button nearest the USB-C port).
  2. Tap RESET (the other button) while still holding BOOT.
  3. Release BOOT.
  4. Run pio run -t upload again.
  5. Tap RESET once when it finishes, to run the firmware.

Command reference

Build & flash

pio runCompile only (no flash)
pio run -t uploadCompile + flash the board
pio run -t upload -vVerbose — shows the exact esptool calls
pio run -t cleanDelete build artifacts (force clean rebuild)
pio run -t upload --upload-port /dev/cu.usbmodem101Flash to a specific port
pio run -t eraseErase the entire flash (then re-upload)

Connect & monitor

pio device listList serial ports — the board is a /dev/cu.usbmodem…
pio device monitorOpen the serial log at 115200 baud
pio device monitor -p /dev/cu.usbmodem101 -b 115200Monitor a specific port / baud
Ctrl+CExit the monitor

The monitor auto-decodes crash backtraces into file:line (monitor_filters = esp32_exception_decoder in platformio.ini), so panics point at real source.

Troubleshooting

Flashing & connection

SymptomFix
No board in pio device listCharge-only cable → swap it. Go straight into the Mac, not a hub. Tap RESET and retry.
“Failed to connect to ESP32-S3”Enter download mode: hold BOOT, tap RESET, release BOOT, upload again.
Port name changed after flashingNormal on the S3 — bootloader and firmware ports differ. Re-run pio device list.
Boot loop / keeps resettingOpen the monitor; a decoded panic prints the crash location. “Brownout” usually means a weak cable/port.
pio: command not foundexport PATH="/opt/homebrew/bin:$PATH"

Display

The display config is the one part that couldn't be tested without the board. Each fix is a one-line change in platformio.ini, then pio run -t upload again. Snap a photo and send it — every one of these is quick once it's visible.

SymptomFix
Blank / black, no glowBacklight or init issue — likely TFT_BL or driver. Ask.
Photo-negative (dark↔bright)Change -D TFT_INVERSION_ON=1=0
Red and blue swappedAdd -D TFT_RGB_ORDER=TFT_BGR
Image shifted / wrapped a few pxGC9107 needs a small offset — note which way it's shifted.
Mirrored / rotated wrongChange tft.setRotation(0) in src/main.cpp (try 1–3).
All black after a few secondsPattern went extinct — tap BOOT for next preset, or IO47 to reseed.

Serial & crashes

On boot you should see, in pio device monitor:

T-QT Pro — Gray-Scott reaction-diffusion
preset 0: Lily  F=0.0175 k=0.0435 Dv/Du=0.45 steps=12

Nothing prints → wrong port, or the board booted before the monitor connected (tap RESET with it open). Gibberish → wrong baud (should be 115200). A backtrace → it's decoded to file:line; paste the whole thing over.

Baked-in presets

Three presets ship in the firmware; Lily is the boot default. The BOOT button cycles through them. Edit include/lily_presets.h and reflash — the format matches the playground's Copy C header export, so you can paste a fresh set straight in.

Customizing

  • Presets: include/lily_presets.hname, F, k, dvdu, palette (0 Ocean · 1 Ember · 2 Orchid · 3 Mono), steps. Preset 0 boots.
  • Resolution: gs::W / gs::H in include/gs_kernel.h (128×128, one cell per pixel — shared by firmware and simulator).
  • Orientation: tft.setRotation(0).
  • Brightness: backlight is full-on; ask for PWM dimming.

← Back to the reaction–diffusion playground