← Lab

Designer & builder 2026

Jump Height

A $20 sensor puck, built from off-the-shelf parts, measures wing-foil jump height and streams it to a Garmin watch. I ran the physics simulations, wrote the firmware and the watch app, and published all of it as open source.

  • Hardware
  • Physics
  • Open source
Cover reading "$20 in parts". An accelerometer trace of one jump, with the free-fall window bracketed as the airtime T.

What it is

Winging, short for wing foiling, means riding a board with a hydrofoil under it while holding an inflatable wing overhead like a sail. Past a certain speed, the foil lifts the board clear of the water, and riders use that lift to jump. My brother rides, and he wanted to know how high his jumps were.

So I built him a thumb-sized waterproof puck, about $20 in off-the-shelf parts, that sticks to the deck of a foilboard. The puck detects jumps on-board and streams height, airtime, and session best over Bluetooth straight to a Garmin watch, with no phone in the loop. The watch app cleared Connect IQ store review in under a day, and his best measured jump so far is 8.1 ft, in the ocean off South Nags Head.

Three renderings of the watch field. On an Epix 2 AMOLED face, white on black: a green dot, the puck id E2C4, "3 jumps", a large "4.2 ft", and "best 5.1 ft · air 1.02s". On an Instinct MIP face, black on grey, the same layout reading "5.0 ft". On a third face, a hollow dot, "JumpHeight", "finding puck", and "requires the board sensor".

The field on both display types, and the state it holds when no puck is advertising. These are the store-listing renders, drawn from the layout code rather than captured: the Connect IQ simulator has no radio, so a screenshot of it can only ever say “finding puck”.

The field is three rows and nothing else. The height is the largest digit that fits the chord at that row, the unit is drawn separately because Garmin’s number fonts carry no letters, and a jump the detector can’t trust never reaches the glass as a measurement. Waiting renders as -- with no unit, because ”— ft” claims a reading that doesn’t exist.

How it works

Integrating acceleration into position drifts by meters within seconds on a cheap sensor, so the puck times the jump. Free fall reads about zero g, the landing is a spike, and the height follows from the airtime between them.

That formula assumes ballistic flight, meaning nothing lifts the rider once the board leaves the water. Kites break the assumption badly. Published work puts kite overshoot at 2.31 times true height, because the kite keeps pulling the rider up through the arc. Nobody had checked wings, where arm force caps the lift a rider can sustain. If wings behaved like kites, every number on the watch face would be inflated, and there would be no point building the puck at all.

I couldn’t collect a hundred thousand real jumps, so I modeled the flight and swept 200,000 simulated ones through the wind speeds and wing angles a rider meets. Wings came back nearly ballistic. Mean overshoot was 1.013 and the 99th percentile 1.062, which puts a typical reading about 1% high and a realistic worst case about 6% high. Five jumps in 200,000 are missed silently, and all five are the same case, a wing sheeted in against 40-knot wind. The boundary is documented in the repo.

1 g0 gT · airtime
The whole measurement is two instants in a noisy signal: leave the water, hit it again.

The detector also flags jumps it can’t measure. Sustained lift shows up directly in the airborne acceleration, so a median above 0.12 g in the air marks a height as inflated. In flights without rotation, that flag separates inflated readings from clean ones perfectly, with an area under the ROC curve of 1.000. Rotation breaks the flag, because spin produces the same signature as lift and can invert the sign, so the gyro subtracts the known spin term before the test runs. That change took the false-positive rate from 0.841 to zero while still catching every case of true lift. A blind band 0.02 g wide remains, measured and published in the repo.

How I built it

One C++ state machine runs on the puck, compiles natively for the test suite, and is mirrored in Python for the simulator. A single JSON file of parameters feeds all three of those and the watch as well, so any threshold exists in one place only. The radio protocol is newline-delimited key=value text, and clients ignore keys they don’t recognize, so a new metric is a new key and pucks already in the water keep working. Mid-project, I retired the browser app I’d built for setup and made the watch the only interface, which cost me my own field-admin tool.

Much of the project was built with Claude Code, with two custom subagents, guard hooks around the bench commands, and a CLAUDE.md of bench rules earned from failures. One of those rules, “no verdict without a measurement”, exists because four boards were declared dead and weren’t. The watch app’s doubt list, a running note of the things I trusted least, predicted the bug that killed its first live connection, by line number.

The data got its sharpest test from a disagreement. My reading of the session screenshots put the 8.1 ft jump on the sound, and the rider said ocean. The FIT file settled it against me. FIT is the recording format the watch writes, and its GPS put him in a tight box off the beach for all 1,806 recorded points. Heights rest on the bench calibration today, and video ground truth is the next gate. Everything is at github.com/joshcrow/Jump-height, MIT licensed.

What I learned

Almost nothing about this product happens on a screen I designed, which is why I built it. The inputs are motion from a board and position from a GPS the rider was already wearing. The hardware is about $20 of open, off-the-shelf parts that anyone could order and build again. The device exists to give a rider a sense he doesn’t have on his own, and it has three rows on a watch face to do that in, read a second at a time by someone wet and moving.

Choosing airtime over integrated position was a product decision before it was a physics one. That choice set the price of the sensor, the size of the firmware, and the shape of the failure. The simulations were how I earned the right to show a height at all. I wouldn’t ship a reading whose error I couldn’t state, and simulation was the only way to state it before a rider found the limit on the water.

The argument about where that 8.1 ft jump happened taught me something smaller. I was confident and I was wrong, and the correction came out of a file the watch had been writing the whole time. I check the recorded file now before I trust a screenshot.