Files
slate32/README.md
T
evan 4e5796fd51 feat(ui): layout toolkit, Lua launcher and touch press events
Apps describe nesting instead of coordinates: /lib/ui.lua borrows CSS block flow,
the box model and auto sizing, and owns hit testing, press capture and the pressed
repaint. The runtime gains require backed by the SD card, on_touch_down/on_touch_up,
text metrics and rounded gradient fills, so the launcher becomes an ordinary Lua app
and the firmware keeps only a fallback screen for an unreadable card.
2026-07-31 22:27:34 -04:00

104 lines
4.3 KiB
Markdown

# 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
```sh
nix develop # provides pio
pio run # build
pio run -t upload # flash over USB-C
pio device monitor # serial logs
```
Copy `sdcard/` to the SD card root: apps live in `/apps/<name>/main.lua` and shared
Lua modules in `/lib`. The launcher is itself an app (`/apps/launcher/main.lua`); the
firmware only draws a fallback screen if it cannot start.
## Lua API
Apps define optional callbacks: `setup()`, `draw()` (~30fps cap), `on_touch_down(x, y)`,
`on_touch_up(x, y)`, `on_touch(x, y)` (tap alias, fired on release), and `on_tick()`
(enabled by setting `TICK_MS`). Call `sys.exit()` to return to the launcher.
`require` reads from the SD card: `/apps/<name>/?.lua` first, then `/lib/?.lua`.
| 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)`, `fillRoundRect(x,y,w,h,r,c)`, `drawRoundRect(x,y,w,h,r,c)`, `fillRectGradient(x,y,w,h,r,top,bottom)`, `fontHeight()`, `textWidth(text)`, `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)`, `listDirs(path)` |
| `sys` | `millis()`, `exit()`, `launch(path)`, `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:
```lua
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.
```sh
nix run nixpkgs#lua -- test/ui_layout.lua # layout, hit testing, capture
nix run nixpkgs#lua -- test/settings_calibration.lua # calibration math and menu flow
```
## UI toolkit (`/lib/ui.lua`)
Apps describe nesting and sizes fall out, borrowing CSS block flow and the box model
without the cascade:
```lua
local ui = require("ui")
local screen = ui.screen(ui.box{pad = 12, gap = 8, color = BLACK, bg = WHITE,
ui.text("settings"),
ui.button{label = "calibrate touch", on_press = calibrate},
})
function draw() screen:draw() end
function on_touch_down(x, y) screen:down(x, y) end
function on_touch_up(x, y) screen:up(x, y) end
```
Sizes are pixels (>= 1), a fraction of the parent's content box (< 1), or `"auto"`
(the default on the flow axis; the cross axis fills the parent). Boxes take `pad`,
`gap`, `row`, `align` and `at` for absolute placement. `color`, `bg`, `radius`,
`gradient` and the press palette inherit from the root, so a theme is set once.
The toolkit owns press capture (release outside cancels), an 80 ms minimum pressed
duration, touch slop, and per-component dirty tracking. Any table with `measure`,
`place`, `draw` and `hit` drops into the tree, so custom components need no buy-in.
Not implemented: scrolling, so a list longer than the screen is unreachable.
## 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).