A visitor reaches toward your prototype. A light brightens and a note sounds. That first response is exciting, but a usable interactive exhibit also has to make sense when the visitor hesitates, moves sideways or walks away. For a designer or a small creative team, those moments are worth testing before the enclosure is committed.
We built a small version on our bench: an ultrasonic distance sensor, a light and a buzzer around a GraftPort RP2040 board running MicroPython 1.23, in a small acrylic enclosure. It demonstrates distance-controlled light and tones. It is not a tested musical instrument or a gesture-recognition system. Its value is that the interaction is small enough to understand and change, and that our test log exposed a useful problem: the outputs kept using an old distance after new readings stopped.
Decide what the visitor is controlling
"Wave your hand to make music" leaves several decisions open. Does the hand trigger a sound once, choose a note while it stays in a zone, or bend the pitch continuously? Should the light follow the sound, or help the visitor find the active area?
These are long-standing design questions. In a 2018 Arduino discussion, a builder needed a hand-responsive art installation inside a translucent column. Detecting a nearby person was not enough; it had to react to the intended hand position and fit the visual design. After oscillator experiments, the author still considered time-of-flight sensing better for that task. Choose the interaction before choosing the sensor.
Our build makes a narrower choice. It maps distance to separate notes and to light brightness, over a configured working range of 25 to 100 cm. Moving a hand closer selects higher notes and brightens the light. That is a candidate interaction for a tabletop exhibit or learning activity; we have not tested it with visitors or across a room.
Build the visible version before hiding the electronics
The parts are the GraftPort board, an RCWL9623 ultrasonic module on I2C0 (address 0x57), a common-cathode Piranha LED module on DIO0, a passive buzzer on DIO1, and an SY7656 battery charge and discharge module. The passive buzzer produces test tones; there is no music-playback speaker. Keeping the roles separate makes diagnosis easier: confirm distance readings first, then light output, then sound. Follow the connector labels on your board revision rather than assuming every four-wire cable has the same pin order.
The battery cable we used is marked red for GND and black for VCC, the reverse of the common convention. Check polarity from the board labels and with a continuity test while unpowered; color is not a wiring instruction. Then switch on the supply and check the power indicator before mounting anything, so a pinched cable or mounting mistake is not later confused with a software problem.
Let the enclosure preserve the interaction
The parts mount on a perforated base with posts, under an acrylic cover held by M3 screws. Keep the sensor face, the light and the controls accessible while trying the interaction, and repeat the hand-position test after fitting each panel. The final geometry is part of the sensing setup, not packaging added at the end.
Two board details matter in the case design. RUN switch 2 must be on when no external watchdog module is fitted, and the battery-level button sits below the battery connector. Keep both reachable during development. We did not measure battery runtime or charging performance inside the enclosure.
Make one distance reading do two understandable things
The software separates board pin mapping, configuration, a scheduler and the device drivers. The interaction lives in one sensor task: get a distance, filter recent accepted values, compute the light output and pick a note. That lets a team change the experience without touching the sensor driver.
The scheduler ticks every 50 ms and the sensor task runs every 200 ms. The distance driver itself waits 120 ms between its command and its read. So a short scheduler tick does not mean a 50 ms reaction. Measure the visible and audible response on the hardware before deciding whether it feels responsive.
The task averages up to three accepted samples and rejects a new value that jumps more than 100 cm from the last accepted one. Those settings smooth abrupt changes, but they also slow how fast the device accepts a genuinely new hand position. Treat them as interaction settings to evaluate, not universal filtering rules.
The light response is strongly nonlinear. An old code comment says "linear," but the implementation raises the normalized distance to the fifth power. At the middle of the working range that gives a duty setting of about 3%, not 50%. That explains why most of the visible change may happen close to the sensor; it does not predict perceived brightness.
The notes are discrete distance bands, not a continuous pitch sweep, and each buzzer call requests a 120 ms tone through a one-shot timer. A sustained, smoothly changing sound would need a different design. A 2018 Moog forum question from an artist shows the same gap: directional sensing and stepped output did not match the continuous response the artwork needed.
When you deploy, upload the whole project, including drivers and scheduler, not only main.py. Then confirm the sensor initializes, that moving a hand changes the readings, and that light and buzzer follow. Our photographs and recordings show the lit assembly and a hand approaching the sensor. They do not quantify latency, distance accuracy or the sound a visitor hears.
Test the moment the hand leaves
Our terminal log is the most useful result of the build. It repeatedly printed:
sensor read: None (ignore)
raw: None filt: 25.57 led duty: 99 buzzer: 987 B5
The raw reading was missing, but the filtered distance stayed at 25.57, the LED duty stayed at 99 and the tone stayed at 987 Hz (B5). The code explains why: missing readings are skipped while the old sample buffer is kept. The outputs keep being driven from stale data.
For an exhibit, decide explicitly what a missing reading means. A short gap might justify holding the last value; a longer one might mean fading the light and muting the tone. Add a time limit based on the age of the last accepted reading, and test it with the real sensor in the real enclosure. That is our proposed fix; we have not built it yet.
The existing button gives a useful comparison. Pausing switches off the light, mutes the buzzer and clears the filter. Resuming lets readings continue. A sensor failure at startup takes a different path: it reports an error and flashes the board LED. Do not assume that startup handling also covers a sensor that drops out while running.
Try it with someone who has not seen the code
Ask a new participant to approach the finished enclosure without coaching. Watch where they put their hand, whether the first response is obvious, and what they expect when they pull away. Then try a quick movement, a slow one and a pause near a note boundary. Log the raw reading, the accepted reading and the outputs alongside those actions, so a confusing moment can be traced to its cause.
If the light barely changes across most of the reach, adjust the mapping before adding effects. If the tone keeps playing after the hand leaves, fix stale readings before adding sensors. If the concept needs a hidden sensor or continuous musical expression, revisit the sensing method and the sound hardware rather than assuming this circuit will scale into it.
The most useful material to bring to the next round is the intended hand movement, an enclosure sketch and a short recording of the current response.