Files
slate32/docs/lua-api-parity.md
T
evan 6f2d618b8d feat: http bindings matching crosspoint-reader, plus generated Lua stubs
The http table copies crosspoint-reader's signatures exactly -- get/head/delete/post/
patch returning (body|nil, status), download taking maxBytes/expectedSize/sha256, the
same 50000 byte body cap and the same -1 for a request that never left the device --
so a script that talks to a server runs on either firmware. docs/lua-api-parity.md
records that, and every other place the two APIs agree, differ for a reason, or differ
because nobody noticed.

Two crosspoint behaviours are deliberately not copied. It reinterprets a string in
argument 2 of a GET as a request body, which turns a mistyped headers table into a
silent protocol error. More seriously it calls setInsecure() on every request, so TLS
is encrypted but unauthenticated on the very path a firmware update would use; this
verifies against the root bundle already sitting in the framework, and the emulator
confirms expired.badssl.com is refused while a wrong sha256 deletes the file.

Downloading exposed two failures worth naming. A 2KB read buffer on the stack tripped
the loop task's canary because a TLS handshake had already spent it, and the
hand-rolled read loop spun forever on a stream that stopped producing -- HTTPClient's
own writeToStream handles both, so the loop is gone and the loop task gets 16KB.

scripts/gen_lua_stubs.py generates stubs/esp32lcd.lua in the same LuaLS format
crosspoint uses, reading annotations off the luaL_Reg tables so a module's docs sit
with its registration. make test runs --check, which crosspoint's copy never wired up.
2026-08-01 17:33:47 -04:00

6.3 KiB

Lua API parity with crosspoint-reader

Both firmwares expose a Lua API to apps on an SD card, and both are ESP32 devices, so a script that only touches files and the network should behave the same on either. This records where they agree, where they differ for a reason, and where they differ because nobody noticed. The last group is a bug list, not a design.

Compared against crosspoint-reader at src/util/lua/LuaBindings*.cpp.

Identical

fs.listDirs, fs.listFiles, fs.exists, fs.readFile, fs.writeFile, gui.width, gui.height, gui.fillRect, gui.drawRect, gui.drawLine, sys.millis, sys.delay, sys.exit, log.debug/info/error, the whole http table, and the draw() / on_tick() callbacks.

Deliberate differences

Area crosspoint-reader esp32-lcd Why
Input input.wasPressed(button) and friends, 8 named buttons input.getTouch, getRawTouch, touched Different hardware. A touch panel has no button names and a button device has no coordinates.
Drawing gui.drawText(font, x, y, text, color, style), getTextWidth(font, text) gui.drawText(text, x, y, color, bg), textWidth(text) crosspoint ships several fonts; this firmware has one built-in font, and needs an opaque background colour because the panel is not e-ink.
Refresh gui.refresh(mode), REFRESH_FULL/HALF/FAST none An LCD has no waveform modes.
Colour COLOR_* constants, 4 grey levels gui.color(r, g, b) returning RGB565 16-bit colour has too many values to enumerate.
Shapes drawRoundedRect + fillRoundedRect one gui.roundRect(...) with gradient and border Fill and border derive from a single distance field, so their edges cannot disagree.
Themes none sys.getTheme/setTheme, /lib/theme.lua Colour panel.
Rotation gui.setOrientation("portrait") sys.setRotation(degrees) persisted, gui.setRotation(0-3) for one frame This device stores rotation in settings and remaps touch to match.
Clock nothing exposed; UTC offset is a C++ setting sys.clockSynced, sys.getTimezone/setTimezone with POSIX TZ rules Timezone here is a stored rule, so os.date() returns local time with DST handled by libc.
Launching launcher is C++ sys.launch(path), launcher is a Lua app The launcher is just another app here.
Modules single-file apps; require unusable require works, package.searchers reads the SD card, /lib on the path Shared code such as ui.lua needs it. crosspoint should adopt this.
BLE ble.* none No BLE use case here yet.
TLS memory TlsScratchLoan lends the framebuffer to wolfSSL none crosspoint is heap-starved; this device has ~280KB free.

Accidental differences — drift, not design

Each of these is the same concept spelled two ways. Fixing them means changing one repo.

Concern crosspoint-reader esp32-lcd Suggested resolution
Entry callback init(), required setup(), optional Pick one name. Neither is better.
Tick interval app.setTickInterval(ms) global TICK_MS Pick one mechanism.
wifi.status() returns a string returns a table of state/ssid/ip/rssi Same name, incompatible types — the sharpest edge here. This repo added isConnected() and localIP() so the crosspoint idioms work either way.
wifi.connect() no arguments, uses stored credentials (ssid, password), saves them Both are wanted: a no-argument reconnect and an explicit join.
fs.readFile cap 50000 bytes 65536 bytes Arbitrary in both.
fs mutation mkdir, rename, remove, removeTree, plus a path-safety check rejecting .. absent This repo is missing them, including the traversal guard.
fs.fileSize, fs.readLineAt present absent Worth porting; readLineAt exists for paging large files.
Timers timer.after/every/cancel + on_timer(id) absent Worth porting.

The http table

Signatures match crosspoint exactly, so scripts port unchanged:

http.get(url, headers?)                -> body|nil, status
http.head(url, headers?)               -> body|nil, status
http.delete(url, headers?)             -> body|nil, status
http.post(url, body?, headers?)        -> body|nil, status
http.patch(url, body?, headers?)       -> body|nil, status
http.download(url, dest, options)      -> bytesWritten | nil, error
http.urlencode(input)                  -> string

status is -1 when the request never left the device. Bodies are capped at 50000 bytes, matching crosspoint, and a larger response yields nil with the real status. http.download requires HTTPS, requires maxBytes, accepts expectedSize and sha256, rejects unknown option keys, and deletes the file if any check fails.

Two behaviours are deliberately not copied:

  1. No argument shifting. crosspoint (LuaBindingsNet.cpp:209) treats a string in argument 2 of get/head/delete as a request body, so a mistyped headers table becomes a silent protocol error. Here that raises.
  2. Certificates are verified. crosspoint calls setInsecure() for every Lua request and for http.download, so traffic is encrypted but unauthenticated — including the path a firmware update would use. This firmware verifies against the root bundle already embedded in the framework (_binary_x509_crt_bundle_start, ~62KB, linked only when referenced). Verified in the emulator: https://expired.badssl.com returns status -1, and a matching sha256 accepts while a wrong one deletes the file.

The cost of matching the signatures is that there is nowhere to put a per-request CA or an insecure escape hatch, so TLS policy is device-wide and scripts cannot weaken it. That is the right trade for a device that flashes itself.

Stub generation

scripts/gen_lua_stubs.py emits stubs/esp32lcd.lua in the same LuaLS ---@meta format crosspoint uses for data/lua/crosspoint.lua, so one editor setup covers both.

The parser differs: crosspoint annotates each C function and reads addFunction(...) calls, while this repo annotates the luaL_Reg table so a module's documentation stays contiguous with its registration.

make test runs --check. crosspoint's generator has no such wiring, so its stub can drift from its bindings silently; that is worth copying back.