Skip to content
Tamagotchi

Tamagotchi

Your physical virtual-pet on an ESP32

0

Created on 3rd June 2026

Tamagotchi

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 BOTH

setSecurityInitKey/RespKey(ENC|ID)

and

NimBLEDevice::startSecurity(connHandle)

in

onConnect

. 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. Raising

Serial.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 coalesced

scheduleScan()

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.

Discussion

Builders also viewed

See more projects on Devfolio