Follows lib/esp32-lua-api: gui -> screen, node -> tree, settings and input
split into screen/sys/touch/buttons. Settings is one provider lighter, with
timezone on Sys and rotation and theme on Gui, which now applies and persists a
rotation in one call. The calibration screen stashes the rotation it borrows
rather than relying on a transient setter.
clang-format had no config here, so adopting the submodule's puts both repos
on the same pointer alignment. .editorconfig is what lua-language-server reads
on save, which is why the Lua tree had drifted between tabs and two widths.
The driver is started once the runtime has allocated, so its first init faces
the same heap as every later one; against a pristine boot heap it wins blocks
that cannot be reassembled. Only the boot sync powers the radio down, so an
explicit connect stays up. Polling a powered-off driver silently re-inits it,
which is what spammed esp_wifi_init failures, so status and loop now check the
mode first. A failed new otherwise reboots with a bare backtrace; the handler
logs the heap instead.
Replaces the firmware's own lua_State, bindings, module loader, node tree
and navigation history with lib/esp32-lua-api. What is left in src/host is
the hardware behind the provider interfaces plus the chrome the firmware
owns: the viewport, the status bar and touch polling.
src/lua became src/host because src is on the include path, so a directory
named lua shadowed the library's <lua/providers.h> and #pragma once then
silently skipped it.
Make sys.launch push the current path and argument, sys.replace switch without pushing, and sys.back restore the previous route. The status-bar chevron uses the same back action, failed child apps return to their caller, and history is capped at eight routes. Remove the redundant Settings back card.
The directory name is what the status bar and the launcher cards display, so Home,
Hello and Settings read as titles without a lookup table. Updates the firmware's home
path, the launcher's self-exclusion and the host tests that load the settings app.
Also brings the bar's next repaint forward when an app renames itself or the frame
rotates. The bar still decides what changed; this only stops the answer waiting most of
a second for the next tick.
sys.launch(path, arg) carries a string to the next app's init(arg), nil when there is
none. States share no memory, so one string is the whole handoff; anything structured
travels as a Lua literal the receiver loads. This is what a screen split across apps
needs to say "collect a password for this network".
sys.setAppName() retitles the status bar, defaulting to the directory name as before. A
setter rather than a declared constant, so one app can retitle per screen. The bar needs
no new invalidation path for it -- the name joins rotation and theme in the cache key --
and clips a name wide enough to reach the memory slot.
Reading a module into one String needs that many bytes contiguous. Once WiFi is up the
largest free block drops to ~40KB, so loading /lib/ui.lua failed -- and readString()
reports allocation failure by silently returning a partial read, which reached Lua as a
syntax error at a random offset. Every app launch was broken while connected.
loadScript() now feeds lua_load() through a 512 byte buffer, so no module needs a
contiguous allocation. readScript(), still used by fs.readFile, verifies the length it
got against the file size rather than trusting it. The launch log carries free and
largest-block so the next heap question is answerable without a rebuild.
The bar repainted itself whole every second. It now compares each field against what
it last painted, adds seconds and a memory percentage, and keys the cache on
gui.getRotation() and ui.themeName so rotation and theme changes still repaint it.
Invalidation lives entirely in Lua; the firmware's push flag and gfx/statusbar.h are gone.
Bindings follow getName/setName/isName, persisted preferences move from sys to a settings
table, and gui.setRotation takes degrees like settings does. A bar that dies mid-run now
keeps its rows reserved rather than silently resizing the running app.
Leaving an app was the app's own responsibility, so one that shipped without
an exit could only be escaped with a reset. The bar now paints a back button
into its leading square and the firmware treats that rect as home, acting on
release so a press sliding into the app cancels. The app it returns to is
/apps/home, which is what it is to the user.
Card grids in home and settings centre left to right as a unit.
The bar is host-owned chrome: the firmware clips apps into a viewport below
it, so no app can paint over it, while /lib/statusbar.lua owns the height,
repaint interval and painting. gui.fullscreen() lets touch calibration take
the physical panel back.
Launcher and settings become grids of square cards (3 across landscape, 2
portrait) via the new gui.setTextSize, ui.label and ui.cardSide. The one
function on the `app` table moves to sys.setTickInterval, alongside the new
sys.appName the bar needs.
The loop spun at full clock between polls, so the CPU was pegged no matter what an app
was doing. delay(1) hands the core to the idle task, which lets the WiFi and lwIP tasks
run promptly and lets real silicon clock-gate in WAITI. Touch reads are throttled to
3 ms by the driver and draws to 33 ms, so nothing observable slows down.
The halted path now idles at 100 ms rather than spinning on a state that cannot change
without a reset.
Not measurable in the emulator: QEMU does not model WAITI, so host CPU stays at 105%
either way. The gain is on hardware.
crosspoint-reader calls it init() and requires it; this called it setup() and treated
it as optional. Same concept, two spellings, so an app could not move between the two
firmwares for no reason worth defending. init() wins because it is also the stricter
contract: a misspelled entry point is now an error instead of an app that starts,
draws nothing, and explains nothing.
Requiring it exposed that error screens were unreadable. fail() painted the message
and the host relaunched the launcher over it on the very next frame, so every Lua
error was serial-only -- which would have made "Missing init()" useless to anyone
holding the device rather than a console.
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.
Nothing in this firmware ever mounted internal flash storage: apps, lib, settings and
themes all live on the SD card. The Arduino default table still reserved 1408K for
spiffs, so a custom table hands that to the OTA slots instead, taking each from 1280K
to 1984K. The 64K tail the slots cannot use goes to coredump, which otherwise logs a
missing-partition error on every boot. Changing the table shifts app0, so the next
flash must be a full one.
The ESP32 has no battery-backed clock, so time is 1970 on every power-up. net.cpp
seeds it from the build timestamp -- the device cannot predate its own firmware, and
certificate validity checks need a plausible clock before SNTP can answer -- then
starts SNTP on each join and latches a synced flag from the notification callback.
sntp_get_sync_status() only reports COMPLETED briefly before resetting for the next
cycle, so polling it would make the clock flap.
sys.clockSynced() is the only new binding a clock UI needs; Lua's os.date() already
reads the same system clock. The launcher shows it, in UTC for now.
Adds a wifi binding over the Arduino API and a settings flow that scans, picks
the strongest AP per SSID, takes a password from an on-screen keyboard, and
reports connection state. Credentials join /settings.lua and reconnect at boot.
Settings are now written through a temp file and rename, and strings are
Lua-escaped, so a password cannot corrupt the file the firmware parses at boot.
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.
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.