diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1177339 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,63 @@ +# slate32 Agent Guidelines + +ESP32 firmware (C++/Arduino) hosting Lua apps off an SD card. C++ owns the panel, touch, +network and persistence; everything user-visible is Lua under `sdcard/`. + +## Build and Test + +```sh +nix develop -c make test # C++ unit tests, Lua tests, and the stub check +nix develop -c pio run # firmware -> .pio/build/esp32-32e/firmware.bin +nix develop -c make stubs # regenerate stubs/slate32.lua after any binding change +``` + +`make test` fails if `stubs/slate32.lua` drifts from the bindings. Regenerate rather than +hand-editing it. + +## Binding Conventions + +Accessors are `getName` / `setName` / `isName`. Bare names are actions (`gui.fillRect`, +`wifi.scan`) or pure conversions (`gui.color`, `http.urlencode`). A missing getter is fine; +an accessor without a prefix is not. + +Namespaces have one job each: `gui` device primitives and the live frame, `ui` widgets and +palette, `settings` persisted preferences, `sys` process and runtime, plus `wifi`/`http`/ +`fs`/`input`/`log`. + +**A setter that requires a follow-up call is a bug in the setter.** `settings.setTimezone()` +applies the TZ itself; `settings.setRotation()` applies the frame and re-clips. The one +unavoidable exception is the theme, because C cannot reload the Lua palette — `ui.setTheme()` +is the seam that pairs them, and apps call that, never `settings.setTheme()`. + +**Persisted intent is not live state.** `settings.getRotation()` is what the user saved; +`gui.getRotation()` is the frame being drawn. They diverge on purpose while an app rotates +the panel transiently (touch calibration does). Never resolve one from the other. + +Settings live in C++ (`src/settings.h`) because the firmware reads rotation and calibration +before any `lua_State` exists, and calibration again on every touch. Lua reaches them through +bindings so there is one writer. + +## Status Bar + +`sdcard/lib/statusbar.lua` paints the top strip; the firmware only clips apps out of it and +calls `draw(home)` on a timer. **Invalidation is entirely Lua's**: `draw()` compares each +field against what it last painted and keys the whole cache on `gui.getRotation()` and +`ui.themeName`. A new app gets a fresh `lua_State`, so an empty cache already means "repaint +everything". + +The one change Lua cannot observe is an app that painted over the bar while fullscreen — +apps toggle through `statusbar.setFullscreen()`, which drops the cache. Do not add +invalidation flags on the C++ side; the facts that drive the bar are only knowable in Lua. + +## Emulator + +Full instructions in `.pi/skills/test-e32r40t-firmware/SKILL.md`. Two things that cost time: + +- The launcher logs `[lua] home ready`, not "launcher ready". It is `/apps/home`, an app like + any other. +- `wait-frame` is an e-ink command and hangs on this board. Use `wait-idle SECONDS` between + captures, and `wait-log` for everything semantic. + +Pure Lua logic belongs in `test/*.lua` against `test/fake_device.lua`, which is the single +definition of the binding surface for host tests. Renaming a binding means editing that file. +Reserve the emulator for the panel, touch and SD.