中文 · English
Hold an M5Stack Cardputer like a racket grip and play badminton by actually swinging it. The game itself runs entirely in the Cardputer's firmware: swing detection, shuttle flight physics, the CPU opponent and scoring all happen on the device. It opens its own WiFi hotspot and serves the game page, so any phone or laptop browser that connects becomes its screen and speakers. There is nothing to install.
The ball goes where you swing. Your character positions itself; you only handle the racket. Swinging up gives a clear or a lift, a downward chop is a smash, a flat swing drives the shuttle, and a gentle touch drops it at the net. The direction of your sideways sweep decides whether the shot goes left or right. Shot quality comes down to timing: a ring closes on screen, and swinging right as it shuts lands you in the sweet spot with a power bonus and the best accuracy. The window is forgiving — anything from 240ms early to 170ms late still connects.
Matches go to 11 points. The CPU has five difficulty tiers, from a rookie that hands you a point every ten exchanges to an "Unbeatable" tier with zero unforced errors and a 60ms reaction that reaches everything. You won't beat it; it's there for practice.
- Power on the Cardputer. Its screen alternates between two QR codes.
- Scan the first one with your phone camera to join the Cardminton hotspot
(it joins silently; your phone keeps using cellular for everything
else). Then scan the second one and the game opens in your browser at
http://192.168.4.1. On a laptop, join the WiFi manually (password
cardminton) and open the same address. - Press Start, or just swing the Cardputer.
You can also try it without hardware: J, K and L swing down, flat and up,
Shift softens the hit, A and D aim, and Space swings with perfect timing.
Add ?demo to the URL for an automatic rally. P opens a debug panel.
Switch the host page to Versus mode. When another racket device joins the
WiFi and connects to ws://192.168.4.1/p2, the right half of the screen
shows the candidate; click it to accept and the view splits.
/p2 takes a fixed binary IMU packet format (documented in
docs/architecture.md), so anything that can produce that stream can act
as a racket. There is a ready-made firmware in firmware/cardminton-racket/
that turns a second Cardputer into the player-two racket. A caveat: I only
own one Cardputer, so that firmware compiles but has never met the host on
real hardware.
cd firmware/cardminton-host
pio run -t upload --upload-port /dev/cu.usbmodem101The web frontend is gzipped and embedded into the firmware binary at build
time (tools/embed_assets.py), so this one command is the entire
deployment — there is no filesystem image to upload separately.
The details live in docs/architecture.md; the short version:
Swing judgment never crosses the radio. Detection runs at 200Hz on the same chip that samples the IMU and feeds the game logic directly. Doing the detection in a browser would put a round of BLE or WiFi batching in front of every swing.
The browser receives 30 state snapshots per second, each carrying the shuttle's position, velocity and a device timestamp. It advances the shuttle from the snapshot to the current frame with the same physics formula the device uses, which is how 30Hz of network turns into 60fps of rendering — by the time the next snapshot arrives, it lands almost exactly on the local prediction.
The timing ring only makes sense if it agrees with the clock the device judges your swing by, so the page keeps an estimate of the device clock via WebSocket ping/pong (taking the lowest-RTT samples) and renders the ring against device time.
The hotspot is deliberately a quiet local network: no DNS, no captive portal. Phones join without any popup and keep their internet on cellular; the game page is reached by plain IP. An earlier version had a captive portal, and dismissing that iOS sheet kept dropping the WiFi with it — not having one turned out to be the reliable design.
There is no machine learning and no calibration step, just a state machine over gyro magnitude:
IDLE → ARMED (>150°/s, gravity latched)
→ FIRE (>240°/s and accel excursion >0.25g)
→ HOLD (250ms, power may only improve)
→ REFRACTORY (500ms)
Direction is computed in the gravity reference frame (gyro-only attitude
integration from the moment the swing arms), which makes it independent of
how you hold the device. Per-frame cost is constant. The design and its
references are in docs/swing-engine.md.
cd native && make run # the C++ core, same sources the firmware builds
npm test # the JS reference implementationThe native suite replays 81 recorded real swings (test/fixtures.json)
through both the C++ engine and the JS reference and requires identical
output — fire time, power and direction must not deviate at all. It also
covers the ballistics, the net-clearance solver, contact picking, and runs
a full simulated match.
firmware/cardminton-host/ host firmware (PlatformIO / Arduino C++)
lib/cardminton/ pure compute library: swing_engine + game_core
(no Arduino deps, shared with the native tests)
src/main.cpp SoftAP + web/WS + IMU task + game tick + screen
tools/embed_assets.py public/ → gzip → embedded in the binary
firmware/cardminton-racket/ companion firmware: a 2nd Cardputer as the P2
racket (compiles; untested on hardware)
native/ native parity tests (make run)
public/ browser renderer: game.js, net.js, i18n.js, sfx
swing.js JS reference swing engine (tests only)
test/ JS test suite + recorded swing fixtures
scripts/export_fixtures.py regenerate fixtures from local recordings
server.mjs static server for frontend dev (npm run dev)
docs/architecture.md architecture and WS protocol
docs/swing-engine.md swing engine design and references
One M5Stack Cardputer ADV (ESP32-S3, BMI270 IMU). A second one is optional for Versus.
MIT — see LICENSE.