September 11 / 2026 · Day 11

Buttons, Buzzer, Haptics

The shared four-gesture control grammar made audible, visible, and tactile.

Arduino ESP32 3.3.10 + M5Unified 0.2.19 + M5GFX 0.2.26 + LVGL 9.3.0Prompt contract and reference frame verified; generated hardware candidate awaits hand review
On this page
Illustration of the M5Stack Stopwatch development kit

Used in this project

M5Stack Stopwatch

Stopwatch Dev Kit (ESP32-S3) · C152

1.75″ round AMOLED · 466 × 466 · ESP32-S3

The build notes: what we tried, what broke, and what stayed. Get the working instructions →

The prompt-first pass

The original build below records how this idea first worked. The durable lesson now targets the M5Stack Stopwatch and lives in the prompt, behavior contract, generalized tests, and hand-review checklist. Generated firmware is evidence for those sources, not the source itself.

Nine days of screens. Today the screen is optional.

The StopWatch kit is shaped like a stopwatch, and stopwatches are operated by feel — thumb on the crown, no eye contact. That’s the design constraint this lesson borrows: every gesture must be recognizable without looking.

Five gestures, two buttons

Two buttons give you two clicks. Time gives you two holds. Simultaneity gives you a chord. Five distinct inputs from two pieces of metal, and every one of them is a timing problem.

M5Unified handles the debouncing and the hold thresholds. What it can’t decide is ownership: when both buttons go down, who gets the event? Day 13’s C25K solved this with a grammar — a gesture owns its buttons until both are released — and this lesson extracts that rule into twenty lines. The chord fires the moment both buttons are down, then swallows everything until full release. No stray clicks on the way out. The same discipline covers holds: a release after a hold is not a click.

Those two leaks — chord-then-click and hold-then-click — are the entire difference between a button grammar and a bug tracker.

The feedback map

The cue table is the part worth arguing about.

Clicks: short, high, distinct pitches for A and B. Holds: longer, lower — an octave down from their clicks. Chord: highest pitch, longest buzz, maximum vibration, unmistakably the big one.

Pitch encodes which button; duration encodes click versus hold; intensity encodes the chord. Three dimensions, five gestures, no collisions. That’s not sound design so much as information design that happens to be audible.

The vibration motor turns off on a scheduled deadline, not a delay() — the loop never blocks, because a blocked loop drops the next button edge, and a dropped edge in a timing grammar corrupts the gesture after it.

What we learned

  • Two buttons are five inputs. Clicks, holds, and a chord — timing multiplies hardware.
  • Gestures need ownership rules. A chord that leaks clicks isn’t a chord; it’s three bugs.
  • Feedback is a code. Pitch for identity, duration for type, intensity for priority — design the mapping, not just the sounds.
  • Never delay() in a timing loop. Every blocking call is a dropped edge waiting for a victim.