Breadboard build guide

Bodhi's Signal Box

Plug in one sensor. The box works out what it is and starts a game that fits it. Swap the sensor and it changes over on its own — no reset, no reflashing, no grown-up.

Board Freenove ESP32-WROVER CAM Kit Ultimate Starter Kit FNK0047 Chip ESP32-D0WD-V3 · 4 MB Port /dev/cu.usbserial-210
Step zero

Unplug the camera first

The OV2640 claims fourteen GPIOs. Once you also set aside the PSRAM, the flash, the microSD slot and the programming UART, four pins are left on the whole board — not enough for a sensor port, four lights and a buzzer.

The camera lifts out of its socket and clips back in whenever you want it. Nothing here is soldered.

0boot
1uart tx
2sd card
3uart rx
4camera
5camera
12free
13free
14sd card
15sd card
16psram
17psram
18camera
19camera
21camera
22camera
23camera
25camera
26camera
27camera
32free
33free
34camera
35camera
36camera
39camera
Every GPIO broken out on the board, and what claims it with the camera fitted. Your board's silkscreen says the same thing in shorthand: - camera, ~ SD card, * PSRAM.
Why the port lives on 13, 32, 33

Those four survivors are exactly where the sensor port and buzzer go. That is deliberate: if you ever refit the camera, it costs you the lights and the display, but the plug-and-play port keeps working.

Build 1 of 2 · the heart of it

The port

Six labelled rows on the breadboard. Three of them carry resistors, and those resistors are the whole reason a five-year-old can plug things in unsupervised.

5V board 5V 3V3 board 3V3 GND board GND S1 1k GPIO 13 S2 10k GPIO 32 20k S3 10k GPIO 33 20k module side board side
No 20 kΩ in the kit? Two 10 kΩ in series is the same thing. S2 and S3 are wired identically — build one, then copy it.
Don't skip the resistors

The 10 k / 20 k dividers mean any kit module, 3.3 V or 5 V, lands safely inside the ESP32's input range. There is no way to hurt the board by plugging something in wrong. Don't substitute 1 kΩ / 2 kΩ — that loads the joystick's 10 kΩ pot hard enough to drag its centre reading down to about a quarter of full scale, and the box stops recognising it.

The 1 kΩ on S1 matters because S1 is sometimes an output — the ultrasonic's trigger — and sometimes an input, the joystick's click, which is a switch straight to ground. The box drives that trigger while it is still working out what's attached, so without the resistor, probing a joystick with its button held down would be a driven pin shorted to ground.

One more consequence worth knowing: because everything arrives through a divider, a 3.3 V motion sensor lands at 2.2 V — below the ESP32's digital-HIGH threshold of about 2.48 V. That is why the code reads S2 and S3 as analog values and compares numbers, rather than trusting digitalRead.

Build 2 of 2 · leave permanently wired

Lights and sound

Four LEDs make the meter Bodhi reads, and the buzzer sings the pitch that tracks it. These never come off the breadboard.

GPIO 23 220 LED 1 GPIO 19 220 LED 2 GPIO 18 220 LED 3 GPIO 4 220 LED 4 GND long leg (anode) toward the resistor
Bottom LED is number 1 — the meter fills upward from it.
GPIO 12 1k S8050 base GND BUZZ 5V
Use the passive buzzer — the one with the open bottom. The active buzzer has a fixed internal oscillator, so it can only beep at a single pitch and can't play the per-sensor jingles or the pitch that tracks the meter.
5 V 3.3 V ground / signal S1 · GPIO 13 S2 · GPIO 32 S3 · GPIO 33
One per module

Make the pigtails

Bundle each module's wires into a single connector so Bodhi pushes in one plug, not five loose jumpers. This is the difference between a toy he can use alone and one that needs you.

Ultrasonic

HC-SR04 · the binoculars
VCC→ 5V
GND→ GND
Trig→ S1
Echo→ S2

Reach the Target. Move your hand closer to push the lights up.

Joystick

dual axis with click
+5V→ 5V
GND→ GND
SW→ S1
VRx→ S2
VRy→ S3

Reach the Target. Push the stick up and down.

Motion

HC-SR501 · the white dome
VCC→ 5V
GND→ GND
OUT→ S3

Wave on Green. Wait for the countdown, then wave fast.

Photoresistor

light level module
VCC→ 5V
GND→ GND
AO→ S2

Reach the Target. Cup your hand over it to fill the meter.

What happens when he plugs one in

How the box knows

Detection runs in priority order, most specific first, and repeats whenever the current module stops answering. Whatever it lands on picks the game — a magnitude game for anything that reports "how much", a timing game for anything that can only say yes or no.

module volts InputDriver raw numbers Gate smooth · debounce Signal level · active Game plays knows sensors knows neither knows games
Drivers know about sensors. Games know about Signal. Nothing knows about both — which is why swapping a module never touches a line of game code.
OrderModuleHow it's recognisedJingle
1UltrasonicAnswers an active trigger pulse — nothing else can imitate thatrising three notes
2JoystickBoth analog lines alive, steady, and sitting near centrebouncy four notes
3PhotoresistorOne analog line alive, the other quiettwo chirps
4MotionIts output goes high — the first wave is the detectionlow–high–mid
5NothingAlways matches, so the box always boots into somethingsleepy two notes
Why the motion sensor is detected by waving

An idle PIR is genuinely indistinguishable from an empty port through a divider — the ESP32's internal pull-up is only specified to 30–80 kΩ, and across that tolerance the two cases overlap. Rather than ship a threshold that works on one board and fails on the next, the box waits for the sensor's output to actually fire. Bodhi plugs in the dome, waves at it, and it announces itself. That can't false-positive, and it reads as magic rather than as a workaround.

Bring it up

Flash and check

  1. Wire the lights, the buzzer and the port. Leave the sensor port empty for now.
  2. Put the diagnostic sketch on the board and watch what it sees.
  3. Plug each module in one at a time and confirm the scanner names it correctly.
  4. Flash the game. Plug something in and play.
# diagnostic first — prints what every port pin is showing
./flash.sh PortScanner
./monitor.sh

# then the game
./flash.sh

With nothing plugged in, the scanner should read like this — both signal lines pulled to zero by their dividers, no echo, and an honest verdict:

S2 pd=   0 (0.00V) pu=1290 | S3 pd=   0 (0.00V) pu=1310 | S1=high | echo=    0 us
   -> nothing detected (if this is the PIR, wave at it)

The pu figures land near 1300 once the dividers are fitted — the internal pull-up fighting your 20 kΩ to ground. Before you fit them, a bare pin reads about 3500 instead. Either way, pd should be zero.

If something's off

Troubleshooting

The board won't boot with a module plugged in

That's a pin conflict, not a loose wire. Check the camera is unplugged, and check nothing is wired to GPIO 0, 2, 5 or 15 — a module holding one of those at the wrong level stops the ESP32 booting at all.

The joystick is detected as a light sensor

Almost always the divider values. At 1 kΩ / 2 kΩ the load pulls the stick's resting centre far enough off mid-scale that the check fails. Use 10 kΩ / 20 kΩ.

Covering the photoresistor empties the meter instead of filling it

Its onboard divider is wired the other way round. Flip kCoveringRaises at the top of DriverLight.cpp — that's the whole fix.

The ultrasonic gives up after a few seconds

It decides it has been unplugged after a long run of unanswered pings. Aim it at the table or a wall rather than across an open room, so it always has something to hear.

Upload dies partway with "the chip stopped responding"

The USB-serial adapter can't sustain the default 921600 baud. flash.sh already retries at 115200 on its own, so let it — it usually gets through on the second attempt.

The buzzer only makes one pitch

That's the active buzzer. Swap it for the passive one, which has an open bottom rather than a sealed sticker.