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.
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.
- camera, ~ SD card, * PSRAM.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.
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.
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.
Four LEDs make the meter Bodhi reads, and the buzzer sings the pitch that tracks it. These never come off the breadboard.
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.
Reach the Target. Move your hand closer to push the lights up.
Reach the Target. Push the stick up and down.
Wave on Green. Wait for the countdown, then wave fast.
Reach the Target. Cup your hand over it to fill the meter.
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.
Signal. Nothing knows about both — which is why swapping a module never touches a line of game code.| Order | Module | How it's recognised | Jingle |
|---|---|---|---|
| 1 | Ultrasonic | Answers an active trigger pulse — nothing else can imitate that | rising three notes |
| 2 | Joystick | Both analog lines alive, steady, and sitting near centre | bouncy four notes |
| 3 | Photoresistor | One analog line alive, the other quiet | two chirps |
| 4 | Motion | Its output goes high — the first wave is the detection | low–high–mid |
| 5 | Nothing | Always matches, so the box always boots into something | sleepy two notes |
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.
# 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.
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.
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Ω.
Its onboard divider is wired the other way round. Flip kCoveringRaises at the top of DriverLight.cpp — that's the whole fix.
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.
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.
That's the active buzzer. Swap it for the passive one, which has an open bottom rather than a sealed sticker.