# Cardminton—playing badminton with a Cardputer as the racket

> Source: <https://www.hackster.io/zhangpengfan6/cardminton-playing-badminton-with-a-cardputer-as-the-racket-1ed594>
> Published: 2026-06-13 06:47:43+00:00

I love badminton, but it's too hot in the summer and I'm always too lazy to go out. So I thought, why not make my own badminton game like the ones in Nintendo games? Luckily, my Cardputer has a built-in gyroscope and accelerometer, so with the help of AI, I created this project.

PlayingSince my English isn't native, the following content was edited into English by AI.

Power it on and the little screen alternates between two QR codes. The

first joins your phone to the hotspot (quietly — the phone keeps its

cellular internet), the second opens the game at 192.168.4.1 in the

browser. From the pocket to a rally takes about half a minute.

Then you swing. Up gives a clear, a downward chop is a smash, a soft touch

drops the shuttle just over the net, and the direction of your sweep aims

the shot left or right. A ring closes around the landing point of the

incoming shuttle; swinging as it shuts is the sweet spot. Once a screen

connects, the device's own display switches from the pairing QR codes to a

little scoreboard — score, serve marker, match state — which sells the

"this little thing is the console" feeling more than I expected.

An earlier version of this project did the obvious thing: stream raw IMU

data over BLE and let a browser compute everything. It worked, but the

Cardputer was reduced to a sensor dongle, and the latency budget was spent

in the wrong place — every swing had to cross BLE batching before the game

could even decide whether it was a hit.

Moving the game onto the device fixed that properly. Detection now runs at

200Hz on the same chip that samples the IMU, so the judgment path involves

no radio at all. The browser's job shrinks to rendering:

The interesting question is how a 30Hz snapshot stream turns into a smooth

60fps picture. The answer is the standard multiplayer-game trick applied to

embedded: each snapshot carries the shuttle's position, velocity and a

device timestamp, and the browser integrates the same physics forward to

"now" before drawing. Both sides run the same deterministic integrator from

the same state, so the next snapshot lands within a couple of pixels of the

prediction and corrections are invisible. Hits change the trajectory, which

is why the tick that launches a shot broadcasts immediately instead of

waiting for the grid.

Timing needed one more piece. The closing ring tells you when to swing, so

it has to agree with the clock the device judges you by. The page estimates

the device clock over WebSocket ping/pong, keeping the lowest-RTT samples,

and renders the ring against device time. On a LAN with 2–10ms round trips

that's accurate to well under a frame.

The swing engineI tried per-user gesture training first: record samples, train a small

classifier. It was unplayable — over half a second of latency and constant

misfires — and after digging into how Wii Sports actually works I concluded

this problem doesn't want machine learning at all. A threshold state

machine that commits on the rising edge of the swing does it better, with

no training and no calibration:

```
IDLE → ARMED  (gyro > 150°/s — latch the gravity vector)→ FIRE   (gyro > 240°/s AND accel excursion > 0.25 g)→ HOLD   (250 ms during which measured power may only improve)→ REFRACTORY (500 ms — covers the follow-through bounce)
```

A few details matter. Gravity is only updated while the device is still,

and frozen during the swing, because accelerometer-based attitude

correction is actively wrong while you're accelerating the device. Vertical

intent comes from strapdown integration: propagate attitude from the moment

of arming using the gyro alone, rotate the accelerometer samples back into

that frame, and integrate vertical hand velocity. That quantity is

literally "is the hand moving up or down", regardless of grip. The

acceleration gate is what rejects false positives — turning the device over

in your hand produces plenty of gyro signal but only about 0.15g of

centripetal acceleration, while even a lazy real swing translates the

device at 0.34g or more.

The firmware engine is a C++ port of a JavaScript original, and the test

suite holds them to identical output across 81 recorded real swings — fire

time, power and direction with zero deviation — plus synthetic cases

(sensor noise, slow tumbling, walking) that must never fire.

Shuttlecock physicsA shuttlecock has a terminal velocity around 6.8 m/s and v² drag, which is

why a smash dies before the back line and a clear falls almost vertically.

The whole flight model fits in one function:

``` js
Vec3 shuttleAccel(const Vec3& v) {const float sp = vlen(v);const float drag = g * (sp / vt) * (sp / vt);return { -drag*v.x/sp, -drag*v.y/sp, -g - drag*v.z/sp };}
```

Shots are built by bisection on top of it. Smashes solve for elevation at a

fixed speed, arcing shots solve for speed at a fixed elevation, and

net-skimming drops search for the lowest elevation whose trajectory still

clears the tape with a margin. The CPU's unforced errors come only from a

per-difficulty dice roll: every CPU shot is verified to clear the net

before launch, with an escalation to a steep rescue lift for shuttles dug

from right under the tape. The "Unbeatable" tier really never beats itself.

Things that went wrongThe captive portal kept killing its own WiFi. The first network design

ran a wildcard DNS and redirected everything, so phones popped the OS

"captive WiFi" sheet with the game inside it. That demos great and plays

terribly: the sheet is a crippled mini-browser, and on iOS dismissing it

often drops the WiFi along with it. The fix was to delete the DNS server

entirely. With no DNS, the OS connectivity probes just fail, the phone

files the hotspot under "local network, no internet", joins silently and

keeps cellular for everything else. The game page is reached by raw IP from

the second QR code.

Every hit caused a 100ms hitch. The net-clearance solver runs on the

order of a hundred trajectory simulations per shot, and it was doing so

inside the 60Hz game tick at the exact moment of contact — precisely where

your eye is pointed. Three changes fixed it: compiling the solver with -O2

instead of -Os, trimming the bisection depths (the precision left over is

still far below the aim noise every shot carries anyway), and pre-solving

the CPU's return at its "reaction" moment instead of at contact. That last

one is the satisfying part: mid-flight the trajectory isn't changing, so

the browser's dead reckoning glides straight through the solver stall and

nobody sees a thing.

M5Unified quietly halves your smashes. The BMI270 powers on at ±8g and

the library never touches the range registers, so real smashes clip. The

firmware sets 200Hz/±16g itself and compensates for the driver's hardcoded

8g conversion scale.

Building it

```
git clone https://github.com/lxhyl/cardmintoncd cardminton/firmware/cardminton-hostpio run -t upload
```

That's the entire deployment: the web frontend (about 250KB gzipped) is

embedded into the firmware image by a PlatformIO pre-script, so there is no

filesystem image or second upload step.

The test suite runs without any hardware:

```
cd native && make run    # C++ core: swing parity + physics + a full matchnpm test                 # JS reference: replay, game logic, strapdown
```

Two playersThe host exposes an open racket endpoint: anything that streams the

documented binary IMU format to ws://192.168.4.1/ p2 becomes player two,

and the screen splits. The connection is host-controlled — the device parks

the newcomer as a candidate and player one clicks to accept it.

firmware/cardminton-racket/ turns a second Cardputer into that racket in

about 200 lines: join the AP, stream the IMU, show the candidate/GO status

on screen. One honest caveat: I own exactly one Cardputer, so this firmware

compiles and follows the protocol but has never met the host on real

hardware. If you have two devices, I'd love to hear how it goes.

What's in the repoEverything is MIT licensed: the firmware, the browser renderer, the native

test harness, the recorded swing fixtures, and the design docs with the

full reasoning — including the approaches that didn't survive.

[Read more](javascript:void(0))
