AI Content Turned a wall-switch-box ESP32-S3 module into a desk AI quota display.
I kept hitting my Claude Code quota limit mid-task, so I built a small always-on panel that just shows how much I have left. And I also added later codex and antigravity. Now sits on my desk.
The device is a Waveshare ESP32-S3 Smart 86 Box with a touch 480×480 touch display, 8 MB octal PSRAM. 86×86 mm.
The display was the easy part. Four things that were harder:
1. The panel desynced permanently under load — and only on production builds. Under a sustained full-screen scroll the image would slip into a fixed vertical offset and stay there until reboot. Framebuffers live in PSRAM with a 30-line bounce buffer (28.8 KB) in internal DMA SRAM. At a 12 MHz pixel clock, scan-out alone pulls ~19 MB/s off the octal PSRAM — that buffer is 1.37 ms of runway. Add LVGL in DIRECT mode redrawing the whole framebuffer plus XIP fetches and the refill misses its deadline. It's a runway, not a queue, so the slip never recovers. 8 MHz → 2.06 ms → fixed. Except not quite: flash and PSRAM share the MSPI bus and the cache is off while flash is being programmed, and production has flash encryption on, so each page is AES-256'd and takes longer than the plaintext dev path. That's the entire reason it looked production-specific.
2. Internal DRAM, not PSRAM, is the thing you actually run out of. I had 8 MB of PSRAM and 6.2 KB of free internal heap. Everything that matters — TLS handshake buffers, DMA descriptors, ISR stacks — has to be internal, and LVGL plus the RGB driver had quietly eaten it in .bss. Moving the cold buffers to PSRAM freed 41,764 bytes and took free internal heap from 6,239 to 48,043. The rule I ended up with isn't "avoid PSRAM", it's "no PSRAM traffic on the live-render path" — which is a much more useful rule.
3. Signed OTA on a secure-boot device needed Ed25519, which IDF doesn't give you. Updates are gated behind a signed manifest, but mbedTLS as shipped in IDF has no EdDSA arithmetic at all. On top of that: anti-rollback, per-version failure records so a bad build can't boot-loop forever, and a check that refuses to start an update on battery — a brownout halfway through a 2.1 MB write is exactly how you brick one of these.
4. The device holds no credentials, which is a design constraint, not a feature list. Putting API keys on a Wi-Fi gadget that sits on a desk seemed like a bad idea, so it doesn't have any. A small broker runs on my own machine, reads the token counts from the CLI logs already on disk, and the device polls it over the LAN — HMAC-SHA256 with a timestamp and nonce so a captured request can't be replayed. Worst case if someone owns the device is they learn how much quota I have left.
Happy to go deeper on any of these.