NestBox firmware: Waveshare ESP32-S3-LCD-1.69

Status: the firmware builds, but has not yet run end to end on a board. BOARD.md lists what is still unverified.

The pet on the real board. It runs the same engine/ as the Mac simulator, serves the same HTTP API and announces itself the same way, so everything that works with the simulator works with the device. The board facts behind it, with sources, are in BOARD.md.

Requirements

Build and flash

cd firmware/esp32
pio run                 # build the firmware and the NESTBOX drive image
pio run -t upload       # flash the firmware and the drive
pio run -t uploadfs     # flash the eggs: the data image, made from ../../pets

Flashing the eggs doesn't touch the firmware. Flashing the firmware doesn't touch the eggs or the saved settings (Wi-Fi, time zone, chosen pet).

Uploading: USB runs through TinyUSB, not the chip's built-in USB serial. PlatformIO asks the running firmware to restart into the ROM loader, which comes back as a different USB port, and waits for it (use_1200bps_touch and wait_for_upload_port in platformio.ini).

If an upload can't find the board (for example, a firmware that crashes before USB starts):

  1. Unplug it, hold BOOT, plug it back in, and let go.
  2. Run pio run -t upload again.
  3. Press RST, or unplug and replug, to start the new firmware.

Close any open serial monitor before uploading.

First board revision: Waveshare moved the buzzer and power pins in V2 (the board has a "V2" QC sticker). For a board without it, use pio run -e nestbox-v1 -t upload. BOARD.md explains why the PWR button is off there.

Which pets go on it

By default only the goose. To add eggs you made yourself (keep them in a folder outside the repo):

NESTBOX_EGGS=~/my-eggs NESTBOX_PETS="goose kitchen-cat" pio run -t uploadfs

Or set custom_pets in platformio.ini. The build copies exactly those eggs (from NESTBOX_EGGS, else the repo's pets/) into data/ and removes any others. An egg whose art may not be redistributed (marked "private": true in its pet.json, or with no license) is left out unless NESTBOX_INCLUDE_PRIVATE=1.

The flash has 7.9 MB for eggs, and they are loaded whole into the 8 MB PSRAM at boot. Eggs store their frames cropped, so a pet is small (the goose is about 200 KB, a large pet about 600 KB). An egg that doesn't fit in PSRAM is skipped, and the console says so.

The NESTBOX drive

Plug the pet into a Mac and a read-only drive called NESTBOX appears, holding:

scripts/drive.py builds the drive from drive/ plus build/NestBox.zip, using scripts/fatimage.py (FAT12 with long names and no partition table). It goes into its own 2 MB flash partition and is flashed with every upload. Anything in drive/ goes on the drive; the build stops if the files don't fit.

Set up Wi-Fi and time

A pet with no Wi-Fi saved opens its own network and a phone sets it up (see Wi-Fi setup in the main README). The USB console does the same by hand:

pio device monitor      # the USB console; type `help`
wifi MyNetwork my-password
wifi "My Network" my-password      (quote a name with spaces)
tz CET-1CEST,M3.5.0,M10.5.0/3      (the time zone as a POSIX TZ rule; tz - clears it)

The device joins the network at once and remembers it. It needs a 2.4 GHz network. For 8 s after it connects, and for 8 s after each press of BOOT, the bottom of the screen shows its name and address, e.g. NESTBOX-3F2A.LOCAL 192.168.1.23. wifi shows the connection and forget clears it.

Internet time starts once Wi-Fi is up. Without a tz, the device uses the UTC offset the Mac companion last reported. With neither, it doesn't know the local time, so day and night follow the companion only.

Other console commands:

Use it

curl http://nestbox-3f2a.local/               # the API page (use your pet's name)
curl -d '{"source":"tests","event":"error"}' http://nestbox-3f2a.local/v1/event

Coding agents (Claude Code, Codex, Cursor…) can read http://<pet's name>.local/agents and connect themselves.

Names

The firmware reads back the name actually in use, republishes it, and shows it on screen and in name. The new name lasts until the next restart, which tries the wanted name again.

The Mac companion

build/nestbox (or the menu bar app) finds the pet by Bonjour. It prefers a device over a simulator (any name containing "simulator" sorts last). To pick one, use --id a13f2a (the id nestbox find lists), the Bonjour instance name (--name "Goose (nestbox-3f2a)"), or --device http://<name>.local. With several pets, the menu bar app pairs by id.

Computer asleep and awake, over the cable

A computer that goes to sleep suspends its USB ports. The pet sees that after 3 s and sends itself {"event": "computer", "state": "asleep"}, so it goes to sleep too; when the bus resumes it sends "awake". This needs no software on the computer.

A phone charger never enumerates the pet as a USB device, so it never reads as asleep. Unplugging the cable while the pet runs on its battery reads as sleep. Activity from the companion over Wi-Fi wakes it either way.

On the device itself

DoIt sendsThe pet
Press PWR{"event": "button", "press": "single"}a pat on the head (as a tap)
Double-press PWR{"event": "button", "press": "double"}dismisses everything waiting for you; with nothing waiting, brings out the next pet
Hold PWR 1 s{"event": "button", "press": "long"}quiet until held again
Tap the case{"event": "tap"}happy
Shake it{"event": "shake"}dizzy

On battery: hold PWR until the screen lights. The firmware holds the power on from then and never switches itself off; unplug the battery, or let it run down, to switch off.

Source layout

Flash layout (partitions.csv, 16 MB):

PartitionOffsetSizeHolds
nvs0x900020 KBsettings: Wi-Fi, time zone, pet, screen
otadata0xE0008 KBwhich app slot boots
app0 / app10x10000 / 0x3100003 MB eachfirmware (two OTA slots)
spiffs0x6100007.9 MBeggs (LittleFS)
drive0xDF00002 MBthe NESTBOX drive (raw FAT12 image)
coredump0xFF000064 KBcrash dumps

The framework is Arduino core 3.3.12 on ESP-IDF 5.5, through PlatformIO's espressif32 8.0.0. The firmware uses about 60% of an app slot and a quarter of the internal RAM (pio run prints both).

At run time one frame buffer (134 KB) sits in internal DMA memory; the 134 KB canvases, the eggs and request bodies sit in PSRAM.

Crash messages and the boot log go to UART0 (GPIO43/44 on the header), not USB, because the USB port runs the firmware's own TinyUSB device.