Tamagotchi
Your physical virtual-pet on an ESP32
Created on 3rd June 2026
•
Tamagotchi
Your physical virtual-pet on an ESP32
The problem Tamagotchi solves
Gochi is a real, physical Tamagotchi — a virtual pet that actually lives on a tiny ESP32-C3 SuperMini wired to a 0.96" SSD1306 OLED, a piezo buzzer, and buttons. Unlike the 90s keychain toys with pre-baked sprite frames, every one of Gochi's 10+ expressions is drawn fully procedurally from primitives (rounded rects, arcs, lines, circles, triangles) — no bitmaps anywhere. Each face has its own idle animation: a happy bob, a sad sliding tear, sleepy floating "Z"s with a breathing chest, an excited fast bounce, an angry tremble, pulsing love hearts with blush, and dead X-eyes.
The pet runs autonomously. It boots into Free Mode, wandering a shared mood (content / playful / grumpy / sleepy / affectionate), picking mood-appropriate expressions every ~10-17s and drifting its mood over minutes. The buzzer plays a short jingle matched to each face, kept in sync no matter what triggered the change.
You can also drive it like a desktop companion. A cross-platform
gochi
CLI mirrors a serial line protocol (show face
,set mood
,get state
,ping
...) and even streams live images — point it at any PNG/JPG and it Floyd–Steinberg-dithers the frame down to 128×64 and renders it on the OLED. A background daemon owns the single connection, auto-detects the device, and exposes a REST API so AI agents (like this one!) can reflect task outcomes on the pet's face. The whole thing works cable-free over Bluetooth LE once paired over USB.Challenges I ran into
Cable-free without a desktop Bluetooth stack to lean on. The C3 is BLE-only (no Classic/SPP), so wireless control is a Nordic UART Service mirroring the serial protocol. The hard bug: NimBLE "just-works" bonding silently connects but never bonds (
getNumBonds()
stays 0, encrypted characteristics unusable) unless you call BOTHsetSecurityInitKey/RespKey(ENC|ID)
andNimBLEDevice::startSecurity(connHandle)
inonConnect
. Pairing is USB-gated for safety — BLE only advertises after a USB command opens a window — and bonds auto-reconnect on boot.Truncated frames over USB. Streaming a base64 image frame kept failing with
mbedtls_base64_decode
returning -44 (INVALID_CHARACTER). Root cause: HWCDC's default 256-byte RX FIFO overruns mid-payload. RaisingSerial.setRxBufferSize(2048)
and the line cap to 1536 fixed it cleanly.Frame-rate bottleneck. Bit-banged I2C capped the OLED at ~15-25 fps. Switching to the C3's hardware I2C peripheral at 400 kHz (routed to GPIO5/6 via the GPIO matrix) unblocked smooth animation.
A wedged radio, not a stale bond. After re-pairing, "no BLE connection" turned out not to be a macOS bond issue but the daemon's long-lived noble stack getting wedged — the connect-failure path called
scan()
while still busy, so it no-op'd forever. Fixed with a coalescedscheduleScan()
and a force stop→start;gochi kill
(fresh CoreBluetooth) became the reliable recovery.No IDE. Everything is built, flashed, and linted from the terminal with
arduino-cli
, a Makefile, and a portable clangd config — reproducible on any machine.Cheer Project
Cheering for a project means supporting a project you like with as little as 0.0025 ETH. Right now, you can Cheer using ETH on Arbitrum, Optimism and Base.
