Commit Graph

72 Commits

Author SHA1 Message Date
evan 6e9cede336 feat: scrolling 2026-08-06 09:05:14 -04:00
evan 0c0365fe45 docs: drop the crosspoint-reader parity comparison
It compared against gui.*, sys.delay, on_tick and a required init(), none
of which exist on either firmware now. The comparison was a stand-in for a
shared API; lib/esp32-lua-api is that API, so agreement is enforced by the
build rather than recorded in prose.
2026-08-05 17:11:13 -04:00
evan b8572fb422 docs: bring the README back in line with the shared runtime
The module table still described gui/input/settings, the tree still named
src/lua and a stubs generator, and the test list named two files that do
not exist. Parity with crosspoint-reader is now a submodule rather than a
comparison document, so the intro says where the platform actually lives.
2026-08-05 17:10:04 -04:00
evan 1adf121dc6 feat(nav)!: own routing in Lua now that the runtime only carries arguments
sys.startApp(path, args) tears the runtime down and starts over from a
file, and keeps nothing else: no history, no title, no app identity. All
of that can ride in the arguments, so it does, and sdcard/.lua/lib/nav.lua
is the single writer of that table.

nav owns the back stack, the bar's title and /.lua/data/<AppId>, which
means the launcher, what "back" reaches and how deep a route nests are
editable on the card rather than in a reflash. Apps call nav.launch,
nav.replace and nav.back; sys.startApp has one caller.

Boot passes no arguments at all, which is how main.lua tells a cold start
from a navigation and opens its own launcher, replacing the home field the
runtime used to read. Arguments cross as JSON, so history is a list of
tables rather than a packed string, and an app passing something JSON
cannot carry sees the error at its own nav call.
2026-08-05 17:04:49 -04:00
evan 575f31e7ee docs(skill): correct emulator sync points and tap targets
Log level prints after the tag, the app's own log precedes the host's running line, wait-frame/sleep are unavailable, and the card grid is two columns.
2026-08-05 11:00:00 -04:00
evan 97abf037b8 refactor(lua)!: camelCase handlers, and type the app tables
Follows the submodule: on_touch_down and friends become onTouchDown, the last
snake_case left in the surface now that handlers are table fields rather than
globals.

main.lua declares SlateApp, this card's contract with its apps, and every app
composes what it fills -- PaintApp is SlateApp plus TouchHandlers, Home is
SlateApp alone -- which is also the only static record of the features an app
needs.
2026-08-05 10:42:06 -04:00
evan 7bc9afae33 refactor(lua)!: migrate to the per-feature namespaces
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.
2026-08-05 10:26:43 -04:00
evan 823a664af7 fix(settings): report a failed scan instead of indexing nil
wifi.scan() returns nil and a reason when the radio cannot allocate, which
took the app down with it.
2026-08-04 21:30:25 -04:00
evan 1aa3a59ca9 feat: give /.lua/main.lua the whole screen and the app contract
The firmware knew where apps, data and modules lived, called four globals,
and painted a status bar into a strip it clipped every app out of. None of
that was its business, and the viewport made the bar something an app could
neither compose with nor replace.

It now loads one file. main.lua mounts the route inside its own node tree,
so the bar is a sibling of the app rather than chrome painted over it: one
layout, one hit test, no inset arithmetic and no invalidation flags on the
C++ side. Apps and main.lua are tables -- they share a lua_State, so globals
would collide -- and a screen changes by rebuilding from node().
2026-08-04 21:15:21 -04:00
evan a60d054359 feat(power): dim the backlight after ten idle seconds 2026-08-04 20:44:13 -04:00
evan 0a620713df fix(net): start SNTP with the saved timezone
configTime() sets TZ to UTC, which undid the saved rule on every join.
2026-08-04 20:44:13 -04:00
evan b3b5a9b2a3 feat(settings): persist the theme through the settings binding
Keeps one writer for saved state: ui.setTheme() writes through
settings.setTheme() and then reloads the palette C cannot see.
2026-08-04 20:44:13 -04:00
evan 9e58e72bf6 build: add a format target and one shared editor config
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.
2026-08-04 13:20:08 -04:00
evan 66c1caa487 fix(fs): only remove a path that exists
SD.remove logs at error level when the path is absent, which the temp-file
pre-clean hit on every ordinary write and the first write hit on its target.
2026-08-04 13:11:32 -04:00
evan 2bc370a4ca feat(ui): restyle the status bar title, back button and wifi icon
The app name now uses the UI font like a card title, with its own baseline so
the larger text stays centred. The back button loses its border, since the
fill already carries the pressed state, and the firmware reports that state so
it can repaint. An offline radio draws faded bars behind a cross rather than
vanishing.
2026-08-04 13:11:28 -04:00
evan af53a668dc feat(net): start wifi after Lua and keep the radio transient
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.
2026-08-04 13:11:23 -04:00
evan fce0dfb70a feat(ble): add a NimBLE provider and fold its controls into Settings
Bluedroid cannot initialize beside the Lua runtime on this heap, so the
provider is NimBLE-Arduino. Scans keep the six strongest devices in a fixed
buffer rather than growing one. The standalone BLE app is gone: it was a
screen of toggles that belongs next to the other device settings.
2026-08-04 13:11:17 -04:00
evan a2997b8f8e chore: .envrc 2026-08-04 10:21:45 -04:00
evan 2724b070d0 chore: lsp + formatting 2026-08-04 10:12:38 -04:00
evan 666c4f3ad8 chore: gitignore stray /.lua/ at repo root 2026-08-03 21:46:01 -04:00
evan 9b0ece8dd3 perf(gui): only compute roundRect corners, DMA the rest
The distance field only matters at the four corner patches; the interior
is a flat fill and the straight edges are 1px lines. DMA those via
fillRect and drawFastH/VLine, and restrict per-pixel computation to
radius*radius per corner.

A bordered full-screen box went from 153,600 per-pixel iterations to 144
(the corners) plus a handful of DMA fills. Cards with radius 6 drop from
~8,100 to ~144 per-pixel iterations.
2026-08-03 21:45:43 -04:00
evan 000eba138a chore: bump esp32-lua-api for embedded platform modules
ui.lua and hints.lua are now compiled to bytecode and linked into the
firmware. The SD card copies (via make sdcard) still shadow them for
development, but a fresh card works without them.
2026-08-03 21:09:54 -04:00
evan 8c8339781a chore: update dep 2026-08-03 20:55:40 -04:00
evan 32280be3a4 chore: bump esp32-lua-api for root border fix 2026-08-03 20:49:18 -04:00
evan 484745445f fix(host): prepare the panel on every app entry, not just at boot
Navigation went straight to the runtime, so a launched app inherited the
previous one's rotation, fullscreen state and viewport, and logged nothing.
Boot and navigation now share prepareForApp/settleNewApp.

Documents the new architecture: the shared platform in lib/esp32-lua-api,
providers in src/host, and the /.lua card layout.
2026-08-03 18:40:10 -04:00
evan 7510bb38a5 feat(sdcard)!: move the card to /.lua and the shared Lua API
Apps live under /.lua/apps with data in /.lua/data, matching the runtime's
paths. ui.lua and hints.lua ship from the submodule through `make sdcard`
rather than a copy that drifts, so theme.lua and the firmware's own ui.lua
are gone.

App-visible changes: text takes a font role rather than a text size, the
theme palette names roles (color/background) instead of fg/bg, ticks are
timer.every, on_press is on_click, and centring is layout alignment now
that textAlign left the node contract.
2026-08-03 18:32:58 -04:00
evan 45abf8a767 refactor: run apps on the shared esp32-lua-api runtime
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.
2026-08-03 18:12:33 -04:00
evan a40d2441f6 build: vendor Lua through the shared esp32-lua-api submodule
Replaces lib/Lua (5.4.7) with lib/esp32-lua-api, which vendors 5.4.8 and
carries the shared runtime the firmware bindings are about to move onto.
2026-08-03 17:48:42 -04:00
evan 7feb00265a Merge pull request 'feat(ui)!: move the widget tree into C++' (#1) from feat/cpp-node-tree into main
Reviewed-on: #1
2026-08-02 23:19:29 +00:00
evan f7c5cc09ba feat(ui)!: move the widget tree into C++
A node was a Lua table of ~625 bytes, of which 21 keys pushed it over a
power-of-two hash boundary and eight were style copies inheritance had
splattered down from its parent. A 400 node screen cost ~250 KB and could not
coexist with wifi's buffers.

The tree now lives in src/ui/layout.h as a 16 byte struct in a flat arena, and
splits by lifetime: Node holds what hit testing and repainting need forever,
Spec holds what only measure/place read and is dropped when layout ends. Style
is sparse and resolved by walking parents, so a node naming no colours costs
nothing. Re-layout rebuilds from Lua rather than retaining the inputs.

    401 nodes:  8218 B steady, 21050 B peak
           Lua: ~250000 B steady

sdcard/lib/ui.lua stays the toolkit and keeps every constructor signature, but
returns integer handles: 627 lines to 374. Composition, the palette and custom
painters are still Lua on the SD card; only primitives now need a reflash.

BREAKING CHANGE: ui constructors return handles, not tables. Use
ui.setText(id, text) and keep per-node app data in a table keyed by id.
2026-08-02 18:48:48 -04:00
evan 932d41aeeb revert(ui): remove scrollable box
Keep raw touch-move events for direct gesture consumers such as Paint. Toolkit screens remain frame-sized to avoid redraw and heap costs.
2026-08-02 17:44:39 -04:00
evan 7c0928d84e feat(ui): add drag gestures and a scrollable box
The firmware now fires on_touch_move with a 2px noise threshold; gesture
intent is the toolkit's 8px slop, since a painting app's stroke starts at
the first real pixel. Past the slop, Screen:move hands the drag to the
nearest scroll ancestor and cancels the widget under the finger.

ui.scroll leans on the app viewport for clipping, so it must reach both
panel edges. Paint and Scroll demo both halves of the gesture split.
2026-08-02 16:59:42 -04:00
evan b51d9c3c4a fix(ui): stabilize partial press redraws 2026-08-02 16:18:11 -04:00
evan 6b6280b72a feat(ui): add reusable on-screen keyboard 2026-08-02 16:07:10 -04:00
evan b2f58118e1 revert(gui): return text rendering to built-in Font 1
Remove OpenFontRender, embedded Meslo assets, custom-font APIs, and their runtime allocation overhead. Keep the taller status layout and compact card grid, with used RAM, aligned faded WiFi bars, and tighter telemetry rows.
2026-08-02 15:26:40 -04:00
evan 9a88d03d23 fix(gui): survive font rendering under low heap
Cache font heights, release FreeType glyph state between apps, and catch renderer allocation failures before they cross Lua and abort the device. Individual low-memory draws degrade to allocation-free Font 1. Show disconnected WiFi as three faded zero-signal bars without expanding the icon footprint.
2026-08-02 15:14:08 -04:00
evan a8602bbd56 refactor(statusbar): simplify memory telemetry
Show used memory in kB without a capacity or icon, and place it immediately left of the right-aligned WiFi indicator.
2026-08-02 14:58:47 -04:00
evan 1cb52997a9 feat(gui): render user-selectable TTF fonts
Embed a Latin-1 Meslo LG S default and use OpenFontRender for anti-aliased text with an explicit pixel size on each draw and measurement call. Apps may replace the one retained face with an SD-backed TTF and restore Meslo with nil. Refresh UI sizing, status telemetry, Settings typography, tests, documentation, and font license notices.
2026-08-02 14:54:49 -04:00
evan 497fab35d9 feat(ui): enlarge the status bar and card grids
Use a two-row 44px status bar with a full-height back button, centered app name, WiFi beside the clock, and free/total RAM below. Expand grids to three portrait and four landscape columns with consistent card text and left-to-right partial rows.
2026-08-02 13:45:00 -04:00
evan 0ca1fd9842 feat(lua): add app navigation history
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.
2026-08-02 13:30:12 -04:00
evan 01dfc4b458 refactor: capitalize app directory names
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.
2026-08-02 13:20:19 -04:00
evan 021b94fca2 feat(lua): pass an argument to init() and let apps name themselves
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.
2026-08-02 12:02:09 -04:00
evan 118e04e340 fix(settings): paint the scan and keyboard notices before the work that blocks
on_tick runs before draw in the same pass, so a screen built for a blocking call was
never painted until the call returned -- on a panel ui.screen() had already cleared.
Scanning looked like a hang on a blank screen. The notice is now painted by the tap
itself and centered on both axes.
2026-08-02 11:20:48 -04:00
evan db7f72d30f fix(lua): stream scripts off the SD card instead of buffering them whole
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.
2026-08-02 11:20:48 -04:00
evan 8f890625f7 docs: add agent guidelines 2026-08-02 10:49:05 -04:00
evan 917f65ae2f feat(ui): give the dark theme purple button faces 2026-08-02 10:49:05 -04:00
evan b5146b8642 refactor(lua): give the status bar its own dirty tracking and split settings out of sys
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.
2026-08-02 10:49:01 -04:00
evan b9b620ad2b feat(ui): add a home button to the status bar, rename launcher to home
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.
2026-08-01 23:13:20 -04:00
evan 6bbe192081 fix(ui): draw button labels without an opaque text background
gui.drawText painted a flat rectangle behind every glyph, so a label on a
gradient card sat in a patch matching only the one gradient row its colour
came from. Omitting the background colour now draws transparent glyphs, and
buttons pass none: the face is repainted above them on every change, so the
label has nothing to erase.
2026-08-01 22:58:19 -04:00
evan 114c8c5f72 feat(ui): add persistent status bar and card grid launcher
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.
2026-08-01 22:50:19 -04:00