Boots to a launcher that lists /apps/<name>/main.lua on the SD card and runs the selected app in a vendored Lua 5.4 with gui, input, fs, sys and log bindings. Settings persist as a Lua table in /settings.lua, covering touch calibration and screen rotation, with a settings app to edit both. Rotation is applied after mapping raw touch into the panel's rotation-0 frame, so turning the UI never invalidates a calibration.
esp32-lcd
Lua-app firmware for the 4.0" ESP32-32E display (lcdwiki E32R40T): ST7796S 320x480 SPI panel + XPT2046 resistive touch + microSD. Apps are plain Lua files on the SD card, launched from an on-screen menu. Inspired by crosspoint-reader's plugin system.
Build & flash
nix develop # provides pio
pio run # build
pio run -t upload # flash over USB-C
pio device monitor # serial logs
Copy sdcard/apps/ to the SD card root (/apps/<name>/main.lua).
Lua API
Apps define optional callbacks: setup(), draw() (~30fps cap), on_touch(x, y),
on_tick() (enabled by setting TICK_MS). Call sys.exit() to return to the launcher.
| Module | Functions |
|---|---|
gui |
width(), height(), clear(color), fillRect(x,y,w,h,c), drawRect(x,y,w,h,c), fillCircle(x,y,r,c), drawLine(x1,y1,x2,y2,c), drawText(text,x,y,fg,bg), setRotation(0-3), color(r,g,b) |
input |
getTouch() -> x,y or nil, getRawTouch() -> raw ADC x,y or nil, touched() |
fs |
readFile(path), writeFile(path, data), exists(path), listFiles(path) |
sys |
millis(), exit(), setCalibration(x0,y0,x1,y1), getRotation(), setRotation(deg) |
log |
info(msg) (serial) |
Colors are RGB565 integers; build them with gui.color(r, g, b).
Settings
/settings.lua on the SD card is a Lua table literal, read at boot and rewritten
by the settings app. It is Lua rather than JSON because the firmware already links
Lua, so it needs no parser on either side:
return {
rotation = 90,
touch = { x0 = 188, y0 = 232, x1 = 3799, y1 = 3800 },
}
Missing file means the built-in defaults are used. apps/settings walks two
crosshairs and saves the result via sys.setCalibration(), and cycles rotation
through 0, 90, 180 and 270 degrees.
Rotation never needs a recalibration: calibration is stored in the panel's
rotation-0 frame (320x480 raw ADC space) and the current rotation is applied
afterwards, so sys.setRotation() is safe at any time. gui.setRotation(0-3)
changes only the current frame; the launcher restores the saved rotation when an
app exits. The glass itself is always portrait, so a rotated UI is drawn
sideways on it.
nix run nixpkgs#lua -- test/settings_calibration.lua # calibration math check
Pin map (fixed by the board)
- Display (VSPI): SCK 14, MOSI 13, MISO 12, CS 15, DC 2, backlight 27, reset = EN
- Touch: shares display SPI, CS 33, IRQ 36
- SD: SCK 18, MOSI 23, MISO 19, CS 5
TFT_eSPI, XPT2046 and SD each grab the SPI controller but share the same wires; only one transfers at a time, which is safe here (see ponytail note in main.cpp).