A slime mold simulation for the ESP32 Cheap Yellow Display, in 2.4, 2.8 and 3.5 inch.
Five hundred agents lay chemical trails and steer toward the strongest one they can sense. That single rule is enough to reproduce how Physarum polycephalum, a single cell with no brain, finds efficient routes between food.
Nothing on the panel is drawn. The branching networks are what the agents leave behind. The trail field decays, but the screen never clears, so what you see includes every route the colony has already abandoned.
Ported from Physarum Lab, which runs the same model in WebGL.
Flash it from your browser: cydphysarum.variable.gallery
Plug the board into a computer with a data USB cable. Open cydphysarum.variable.gallery, pick your board, pick a seed, press install. Nothing to download, and no toolchain.
Web Serial is required. Recent Chrome, Edge, Opera and Firefox have it. Safari and
mobile browsers do not, so the page also hands you the firmware, a seed file, and
the one esptool command that writes both.
On Linux the browser needs permission for the serial device:
sudo usermod -aG dialout $USERThen log out and log in again. A new terminal is not enough, because the browser must restart before it picks up the new group.
| Env | SKU | Size | Resolution | Controller | Backlight |
|---|---|---|---|---|---|
cyd24 |
ESP32-2432S024R | 2.4" resistive | 320 × 240 | ILI9341 / ST7789 | GPIO 21 |
cyd24c |
ESP32-2432S024C | 2.4" capacitive | 320 × 240 | ILI9341 / ST7789 | GPIO 27 |
cyd28 |
ESP32-2432S028R | 2.8" resistive | 320 × 240 | ILI9341 | GPIO 21 |
cyd35 |
ESP32-3248S035R | 3.5" resistive | 480 × 320 | ST7796 | GPIO 27 |
Every one of these is a plain ESP32-WROOM-32: dual core, 240 MHz, 4 MB of flash, no PSRAM. They wire the display the same way, so one binary drives any of them. It picks the init sequence, the backlight GPIO and the simulation tuning from the board you name.
You do have to name it. Read Notes for CYD owners for why.
The seed fixes the colour palette and the character of the colony. The same seed always grows the same organism, so a specimen you like is a number you can write down and flash again.
The installer page grows a colony in the background while you choose. The board and the page build their palettes from the same generator, so the seed you pick is the palette you get on the panel. The exact shape differs, because the panel grid is not the size of a browser window.
The seed is not compiled in. It lives in a 4 KB config partition that the
installer writes, so a change never needs a rebuild. Three modes:
- a seed you choose,
- a random seed, picked once at install time,
- a new random seed on every power-on.
pio run -e cyd35 -t upload # 3.5" ESP32-3248S035R
pio run -e cyd28 -t upload # 2.8" ESP32-2432S028R
pio run -e cyd24 -t upload # 2.4" ESP32-2432S024R
pio run -e cyd24c -t upload # 2.4" ESP32-2432S024C
pio device monitorAdd --upload-port /dev/ttyUSB0 (or COMx) if the wrong port is picked. Hold BOOT
during the upload if the board is not detected.
The env sets only the default board. A config sector written by the web installer overrides it, so a board flashed from the browser is right whichever env built the image.
pio run -t upload does not touch the config partition, so a seed set by the
installer survives a reflash. To go back to a random seed:
esptool.py erase_region 0x300000 0x1000Each agent holds a position and a heading. Every frame it does four things:
- Read the trail field at three points ahead: left, centre, right.
- Turn toward the strongest of the three.
- Move one step and deposit into the trail field.
- Eat any food under it.
The trail field then blurs by one cell and decays a few percent. That is the whole model. Networks appear because a deposit makes a route more attractive, and an attractive route collects more deposits.
Left alone, the colony settles into stripes. Two mechanisms stop it. Food clusters respawn into empty areas, which drags the colony across the panel. A hash of the trail field is sampled every few seconds, and when it stops changing, the agents scatter and fresh food scatters with them.
The model is in src/SlimeMold.cpp. The installer page runs
the same model in JavaScript, in
web/src/lib/physarum.ts.
include/Panel.h multi-board driver and the board profile table
src/Panel.cpp
include/Palette.h seeded palette, shared with the installer page
src/Palette.cpp
include/DeviceConfig.h seed and board record in the config partition
src/DeviceConfig.cpp
include/Tuning.h simulation tuning, chosen at runtime from panel size
include/SlimeMold.h simulation
src/SlimeMold.cpp the agent model
src/main.cpp bring-up and the serial boot report
partitions.csv factory app plus a 4 KB config partition at 0x300000
platformio.ini one env per board
scripts/build-firmware.sh builds and packages the web-flashable image
web/ SvelteKit installer, cydphysarum.variable.gallery
Two pairs of files implement the same function twice, once in C++ and once in TypeScript. Change one and you must change the other.
| C++ | TypeScript | Why |
|---|---|---|
src/Palette.cpp |
web/src/lib/palette.ts |
The page previews the palette the board will use |
src/DeviceConfig.cpp |
web/src/lib/installer.ts |
The page writes the config record the board reads |
Both are verified rather than assumed. The palette generator produces identical
RGB on an ESP32 and in Node across eight seeds, including 0 and values above
2³¹. The config record is checked byte for byte, 504859530100020040e2010001000000e8069f4d
on both sides.
The CRC in the config record is a plain CRC-32, written out longhand on both sides
instead of calling the ESP ROM crc32_le, so the two are provably the same
function. They meet only on real hardware, where a mismatch would be silent.
SeededRandom in the C++ port reproduces JavaScript number semantics on purpose.
Every intermediate is kept in the width JS would use, int32 for the state and
uint32 for the result, with the products computed in int64 before truncation. Do
not simplify those casts.
src/main.cpp prints a stable key=value block that the installer parses:
physarum:begin
panel=CYD 3.5"
sku=ESP32-3248S035R
panel_source=config
panel_id=0x7FDF00
resolution=480x320
seed=123456
seed_source=configured
physarum:end
panel_source is config when the installer wrote it, or build-default when the
env did. seed_source is configured or random. panel_id is diagnostic only,
and on these boards it is expected to be meaningless.
One packed little-endian record, at 0x300000:
| Offset | Size | Field |
|---|---|---|
| 0 | 4 | magic 0x53594850 (PHYS) |
| 4 | 2 | version (1) |
| 6 | 2 | board: 0 build default, 1 CYD 2.8", 2 CYD 3.5", 3 CYD 2.4"R, 4 CYD 2.4"C |
| 8 | 4 | seed |
| 12 | 4 | flags, bit 0 = reseed on every boot |
| 16 | 4 | CRC-32 of the first 16 bytes |
Board values are append-only, so a sector written by an older installer keeps its meaning. A bad magic, version or CRC counts as blank, which means a random seed and the board compiled into the build.
cd web
npm install
npm run dev # http://localhost:3000
npm run build # static site in web/buildTo rebuild the image the site serves:
./scripts/build-firmware.shThis merges bootloader, partition table and application into one image at offset
0x0 with esptool merge_bin, and writes it into web/static/firmware/ next to
firmware.json. That file carries the config partition offset, so the offset is
defined in one place. The artefacts are committed, so any static host can serve the
site without a C toolchain.
The merged image uses DIO flash mode, inherited from the framework bootloader
through --flash_mode keep. Every CYD flash chip accepts it. Local pio run
builds still use QIO.
Built by Shahab Nedaei for variable.gallery.
Sibling of Physarum Lab, which runs the same organism in WebGL across Euclidean, hyperbolic and Nil geometries.
MIT. See LICENSE.

