A multi-unit social toy for the Cheap Yellow Display (CYD). Flash the same sketch to two or more units and they discover each other over ESP-NOW — no router, no WiFi join, no pairing — and form a connectionless mesh.
What the units share with each other:
- Shared whiteboard. Draw on the black canvas and your strokes stream live to every peer as vector segments, replaying on their screens. A spectrum + greyscale color picker runs along the bottom; "clear" (top-right) wipes the canvas on every unit.
- Emoji reactions. Tap an emoji in the bottom toolbar to broadcast it. Every unit shows it big and centered for a couple of seconds and plays a little per-emoji jingle through the speaker.
- Proximity. Each unit estimates how close its peers are from the radio signal strength (RSSI) of their packets, smoothed over time, and shows near/far on the tools screen.
- Editable 3-character id per unit, saved to flash so it survives reboots.
The top-left corner toggles between the home (doodle) view and the tools view.
You need two or more CYD units to see anything interesting — a single unit just talks to nobody.
- Board: Cheap Yellow Display, ESP32-2432S028 (ESP32S, no PSRAM). These are sold under many names; look for the 2.8" 320×240 SPI TFT with a resistive touch panel and an ESP32 on the back.
- Speaker (optional): the on-board amplifier on GPIO 26 plays the emoji jingles. Everything else works without it.
⚠️ Display-driver variant trap. CYD boards ship with one of two TFT panels — ILI9341 or ST7789 — and which one you have does not reliably track the USB-connector count or the box label. This project's reference unit is ILI9341. If your screen is blank, garbled, mirrored, or badly mis-colored, you have the other panel — set the driver toST7789in your TFT_eSPI setup (see below).
| Function | Pins |
|---|---|
| Display (SPI) | MISO 12 · MOSI 13 · SCLK 14 · CS 15 · DC 2 · RST −1 · BL 21 |
| Touch (XPT2046) | CLK 25 · MISO 39 · MOSI 32 · CS 33 · IRQ 36 (own SPI bus) |
| Speaker amp | GPIO 26 |
Install the Arduino IDE and add the ESP32 board package (3.x) via Boards Manager.
- TFT_eSPI (display)
- XPT2046_Touchscreen (touch)
WiFi, esp_now, esp_wifi, and Preferences are part of the ESP32 core — no
separate install.
TFT_eSPI is configured by editing a setup header inside the library itself, not from this sketch — so if you already have a working CYD setup, it'll just work. If you're starting fresh, the community reference ESP32-Cheap-Yellow-Display has step-by-step display-config instructions (and ready-made setup files) for this exact board.
The only project-specific things to get right:
- Driver: select
ILI9341(this project's reference unit) orST7789for your panel — see the variant note above. If the screen is blank, garbled, mirrored, or mis-colored, switch to the other one. - Pins: use the display pins from the Pinout table above.
- Touch: leave touch unconfigured in TFT_eSPI — this sketch drives the XPT2046 panel separately via the XPT2046_Touchscreen library.
- Open
CYD_Doodler.inoin the Arduino IDE. (The.inofilename must match its folder name — keep the folder calledCYD_Doodler.) - Select board ESP32 Dev Module and the right serial port.
- The default partition scheme is fine (this sketch has no BLE; the binary is small).
- Upload. Serial monitor runs at 115200.
Flash the same sketch to every unit.
- All units must agree on
WIFI_CHANNEL(top of the sketch, default1). If your units can't see each other, check they're on the same channel. - ESP-NOW is single-hop: units have to be within direct radio range.
- On first boot each unit picks a random-ish 3-char id from its MAC. Tap the top-left corner to open the tools screen and edit it with the up/down arrows; it's saved to flash.
| File | What it is |
|---|---|
CYD_Doodler.ino |
The whole sketch. |
doodler_types.h |
Shared structs/enums (kept in a header on purpose — see the note at its top). |
A few project-specific notes if you want to hack on it:
- Transport. Pure ESP-NOW broadcast —
WiFi.mode(WIFI_STA)+WiFi.disconnect()+ a fixed channel; it never joins an access point. Each unit beacons a few times a second to the broadcast peerFF:FF:FF:FF:FF:FF. There's no SSID or password anywhere. - Receive path. The recv callback (core-3.x signature,
onRecv(const esp_now_recv_info_t*, const uint8_t*, int)) runs in the WiFi task, not the main loop. Per-packet RSSI comes frominfo->rx_ctrl->rssiand the sender MAC frominfo->src_addr. The callback only touches shared state (the peer table and stroke buffer) inside aportMUXcritical section; all TFT drawing happens inloop()to keep it off the WiFi task. - Rendering. No PSRAM, so a full 320×240 16-bit sprite won't allocate. The
framebuffer is an 8-bit
TFT_eSprite(~75 KB); each frame composites into it andpushSprite()s once to avoid flicker. Canvas/pen color is therefore 8-bit (3-3-2, ≈256 colors), and a pen color travels over the air as a single packed byte. - Types live in
doodler_types.hon purpose. Once the.inocontains template functions, the Arduino IDE injects auto-generated prototypes at the very top of the file — above any type defined in the.ino— so any struct/enum used in a function signature must live in the header, or you get "not declared in this scope" errors. - Persistence. The 3-char id is saved with
Preferences(NVS), namespace"doodler", key"name". - Sound.
tone(26, …)/noTone(26); jingles are short blocking note sequences, which is fine for occasional events.
MIT — free to use, modify, and redistribute; please keep the copyright and license notice. Copyright © 2026 Gabriel M Schuyler.
