I built a Halloween box that opens its lid when you walk up, says something rude, and glows from inside. It started as a pizza box, became a shoebox, and this year it’s dressed up as a giant wrapped hard candy. The repo is still called PizzaBox.

Code, wiring diagrams and a step-by-step build guide: https://bitbucket.org/pablodiazgutierrez/halloweenpizzabox

The electronics inside the box: breadboard with an ESP32 and amplifier, a two-cell battery shield, and the speaker

What it does⌗

  • An HC-SR04 ultrasonic sensor pings every 60 ms. A brisk approach triggers the lid in about 60 ms; slower presence needs three readings.
  • An SG90 servo lifts the lid in one of five “personalities”: hesitate, slow creak, fast pop, tremble, and a fake-out that opens halfway, slams shut, and then opens for real. Fast approaches get the sudden ones.
  • A MAX98357A I2S amplifier plays a creak or a thump, then one of 54 voice lines. 92 clips in total, stored as MP3 frames in the ESP32’s flash (about 820 KB, no SD card).
  • A WS2812 LED ring follows the lid angle, so a fake-out flashes and goes dark and a tremble pulses. Each LED flickers on its own, like a candle.
  • Everything runs on two 18650 cells, with no computer attached.

The parts cost about US$40–60 if you buy them new. Most of them come in Arduino starter kits.

A few of the interesting bits⌗

Ignoring the scenery. An ultrasonic sensor pointed at a sidewalk also sees hedges, walls and parked cars. The box treats anything that stays at one distance for two seconds as background and ignores it, along with everything behind it. Only readings at least 5 cm closer than the scenery count. It’s background subtraction for a one-pixel depth camera:

// Something that stays at one distance for BACKGROUND_LEARN_MS is scenery: readings at or
// beyond it are ignored, so the prop can face a wall.
bool isBackground(int cm) {
  return backgroundCm > 0 && cm >= backgroundCm - STABLE_TOLERANCE_CM;
}

A nice side effect: a kid who freezes in front of the box for two seconds becomes scenery too. Taking another step forward sets it off again.

Never trusting one echo. The HC-SR04 throws out the occasional garbage reading, and a missing echo is not the same thing as “far away.” No single reading can open the lid. A fast approach needs a drop worth at least 120 cm/s followed by a second close reading that doesn’t bounce back. A hand right in front needs two readings under 25 cm. Anything else needs three in a row.

Sound that never blocks the lid. Audio runs in its own FreeRTOS task that decodes MP3 frames and streams them over I2S. The motion code talks to it through a one-slot queue with xQueueOverwrite, so a new cue always replaces an old one and the servo never waits on the speaker:

// One mailbox: the newest event wins. Never wait in the motion/sensor path.
return xQueueOverwrite(soundQueue, &request) == pdPASS;

LEDs without a library. The light ring gets its own task too. It builds WS2812 bit timings by hand and pushes them through the ESP32’s RMT peripheral every 20 ms, so the flicker keeps going while the main loop is busy with the servo.

Tests for a Halloween prop. tests/run.sh compiles the sketch on a laptop against small Arduino and FreeRTOS shims and runs over 1,100 assertions on the trigger logic, plus the commands, audio and light, with the address and undefined-behavior sanitizers on. Overkill for a candy box? Probably.

Complete wiring diagram

The voice lines⌗

The sound effects are CC0 recordings. The voices come from macOS’s built-in say, which is licensed for personal use only, so the repo has the text of the lines but not the audio. On a Mac, python3 tools/prepare_sounds.py renders them and the sketch picks them up. Without a Mac the box still creaks, rattles and burps. It just doesn’t talk.

Some of my favorites:

  • “Do not eat. This candy eats you.”
  • “Please rate your scare from one to five stars.”
  • “This is a recording. The monster is on its break.”
  • “Nutrition facts. Zero calories. Zero survivors.”
  • “Lid closing. Mind your fingers.”

We picked the voices by ear through the box’s own 2" speaker. Pitched-down “monster” voices that sounded great on a laptop turned to mush. The ones that survived are Reed (slightly deepened), Daniel, Samantha, and Jester for the laughs.

Lessons learned the hard way⌗

The reference doc has the full list. The highlights:

  • Charge-only USB cables. The board’s LED is on but no serial port shows up. The cable has no data lines.
  • The Arduino IDE’s serial monitor reset this board in a loop. The repo’s monitor.sh holds RTS and DTR inactive so connecting doesn’t reboot it.
  • Running the servo off USB power made the board drop off USB every time the lid moved. The servo needs its own 5 V supply and a shared ground.
  • An SG90 stalled against the box strips its plastic gears. Find your angles with the arm free, then back off 5–10° from anywhere the servo hums.
  • A dead amplifier looks exactly like bad wiring. The bit clock read “high 0% of the time,” and it turned out the amp itself was holding it low. Swapping in a known-good amp settled it.
  • macOS say reads “Shh” and “Mmm” as letters. Write actual words.
  • Phone photos carry GPS coordinates. Strip the metadata before you publish pictures of your house’s Halloween prop.

How it was built⌗

I wrote most of this with Claude Code. I did the hardware: soldering, wiring, cardboard, hot glue, and walking toward the box over and over. Claude wrote the firmware, the tests, the scripts that draw the diagrams, and the build log. The log was more useful than I expected. When the amplifier went silent, it held every measurement we took, so each new session picked up from what we already knew instead of starting over.

Build your own⌗

The build guide goes from an empty breadboard to a working box in seven stages, testing each part before adding the next: flash the ESP32, move the lid, add the sensor, add sound, go battery-powered, add light, then mount it all and tune it. Code and docs are MIT licensed.

Happy Halloween. If you build one, I’d love to see it: ihaveajob at gmail dot com.