{"slug": "cardminton-playing-badminton-with-a-cardputer-as-the-racket", "title": "Cardminton—playing badminton with a Cardputer as the racket", "summary": "A developer created Cardminton, a badminton game for the M5Stack Cardputer that uses its built-in gyroscope and accelerometer to detect swings, smashes, and drops. The game runs entirely on the device at 200Hz, with a browser rendering the 3D scene via a 30Hz snapshot stream. The project avoids machine learning in favor of a threshold state machine for swing detection, achieving low latency and accurate gameplay.", "body_md": "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.\n\nPlayingSince my English isn't native, the following content was edited into English by AI.\n\nPower it on and the little screen alternates between two QR codes. The\n\nfirst joins your phone to the hotspot (quietly — the phone keeps its\n\ncellular internet), the second opens the game at 192.168.4.1 in the\n\nbrowser. From the pocket to a rally takes about half a minute.\n\nThen you swing. Up gives a clear, a downward chop is a smash, a soft touch\n\ndrops the shuttle just over the net, and the direction of your sweep aims\n\nthe shot left or right. A ring closes around the landing point of the\n\nincoming shuttle; swinging as it shuts is the sweet spot. Once a screen\n\nconnects, the device's own display switches from the pairing QR codes to a\n\nlittle scoreboard — score, serve marker, match state — which sells the\n\n\"this little thing is the console\" feeling more than I expected.\n\nAn earlier version of this project did the obvious thing: stream raw IMU\n\ndata over BLE and let a browser compute everything. It worked, but the\n\nCardputer was reduced to a sensor dongle, and the latency budget was spent\n\nin the wrong place — every swing had to cross BLE batching before the game\n\ncould even decide whether it was a hit.\n\nMoving the game onto the device fixed that properly. Detection now runs at\n\n200Hz on the same chip that samples the IMU, so the judgment path involves\n\nno radio at all. The browser's job shrinks to rendering:\n\nThe interesting question is how a 30Hz snapshot stream turns into a smooth\n\n60fps picture. The answer is the standard multiplayer-game trick applied to\n\nembedded: each snapshot carries the shuttle's position, velocity and a\n\ndevice timestamp, and the browser integrates the same physics forward to\n\n\"now\" before drawing. Both sides run the same deterministic integrator from\n\nthe same state, so the next snapshot lands within a couple of pixels of the\n\nprediction and corrections are invisible. Hits change the trajectory, which\n\nis why the tick that launches a shot broadcasts immediately instead of\n\nwaiting for the grid.\n\nTiming needed one more piece. The closing ring tells you when to swing, so\n\nit has to agree with the clock the device judges you by. The page estimates\n\nthe device clock over WebSocket ping/pong, keeping the lowest-RTT samples,\n\nand renders the ring against device time. On a LAN with 2–10ms round trips\n\nthat's accurate to well under a frame.\n\nThe swing engineI tried per-user gesture training first: record samples, train a small\n\nclassifier. It was unplayable — over half a second of latency and constant\n\nmisfires — and after digging into how Wii Sports actually works I concluded\n\nthis problem doesn't want machine learning at all. A threshold state\n\nmachine that commits on the rising edge of the swing does it better, with\n\nno training and no calibration:\n\n```\nIDLE → 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)\n```\n\nA few details matter. Gravity is only updated while the device is still,\n\nand frozen during the swing, because accelerometer-based attitude\n\ncorrection is actively wrong while you're accelerating the device. Vertical\n\nintent comes from strapdown integration: propagate attitude from the moment\n\nof arming using the gyro alone, rotate the accelerometer samples back into\n\nthat frame, and integrate vertical hand velocity. That quantity is\n\nliterally \"is the hand moving up or down\", regardless of grip. The\n\nacceleration gate is what rejects false positives — turning the device over\n\nin your hand produces plenty of gyro signal but only about 0.15g of\n\ncentripetal acceleration, while even a lazy real swing translates the\n\ndevice at 0.34g or more.\n\nThe firmware engine is a C++ port of a JavaScript original, and the test\n\nsuite holds them to identical output across 81 recorded real swings — fire\n\ntime, power and direction with zero deviation — plus synthetic cases\n\n(sensor noise, slow tumbling, walking) that must never fire.\n\nShuttlecock physicsA shuttlecock has a terminal velocity around 6.8 m/s and v² drag, which is\n\nwhy a smash dies before the back line and a clear falls almost vertically.\n\nThe whole flight model fits in one function:\n\n``` js\nVec3 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 };}\n```\n\nShots are built by bisection on top of it. Smashes solve for elevation at a\n\nfixed speed, arcing shots solve for speed at a fixed elevation, and\n\nnet-skimming drops search for the lowest elevation whose trajectory still\n\nclears the tape with a margin. The CPU's unforced errors come only from a\n\nper-difficulty dice roll: every CPU shot is verified to clear the net\n\nbefore launch, with an escalation to a steep rescue lift for shuttles dug\n\nfrom right under the tape. The \"Unbeatable\" tier really never beats itself.\n\nThings that went wrongThe captive portal kept killing its own WiFi. The first network design\n\nran a wildcard DNS and redirected everything, so phones popped the OS\n\n\"captive WiFi\" sheet with the game inside it. That demos great and plays\n\nterribly: the sheet is a crippled mini-browser, and on iOS dismissing it\n\noften drops the WiFi along with it. The fix was to delete the DNS server\n\nentirely. With no DNS, the OS connectivity probes just fail, the phone\n\nfiles the hotspot under \"local network, no internet\", joins silently and\n\nkeeps cellular for everything else. The game page is reached by raw IP from\n\nthe second QR code.\n\nEvery hit caused a 100ms hitch. The net-clearance solver runs on the\n\norder of a hundred trajectory simulations per shot, and it was doing so\n\ninside the 60Hz game tick at the exact moment of contact — precisely where\n\nyour eye is pointed. Three changes fixed it: compiling the solver with -O2\n\ninstead of -Os, trimming the bisection depths (the precision left over is\n\nstill far below the aim noise every shot carries anyway), and pre-solving\n\nthe CPU's return at its \"reaction\" moment instead of at contact. That last\n\none is the satisfying part: mid-flight the trajectory isn't changing, so\n\nthe browser's dead reckoning glides straight through the solver stall and\n\nnobody sees a thing.\n\nM5Unified quietly halves your smashes. The BMI270 powers on at ±8g and\n\nthe library never touches the range registers, so real smashes clip. The\n\nfirmware sets 200Hz/±16g itself and compensates for the driver's hardcoded\n\n8g conversion scale.\n\nBuilding it\n\n```\ngit clone https://github.com/lxhyl/cardmintoncd cardminton/firmware/cardminton-hostpio run -t upload\n```\n\nThat's the entire deployment: the web frontend (about 250KB gzipped) is\n\nembedded into the firmware image by a PlatformIO pre-script, so there is no\n\nfilesystem image or second upload step.\n\nThe test suite runs without any hardware:\n\n```\ncd native && make run    # C++ core: swing parity + physics + a full matchnpm test                 # JS reference: replay, game logic, strapdown\n```\n\nTwo playersThe host exposes an open racket endpoint: anything that streams the\n\ndocumented binary IMU format to ws://192.168.4.1/ p2 becomes player two,\n\nand the screen splits. The connection is host-controlled — the device parks\n\nthe newcomer as a candidate and player one clicks to accept it.\n\nfirmware/cardminton-racket/ turns a second Cardputer into that racket in\n\nabout 200 lines: join the AP, stream the IMU, show the candidate/GO status\n\non screen. One honest caveat: I own exactly one Cardputer, so this firmware\n\ncompiles and follows the protocol but has never met the host on real\n\nhardware. If you have two devices, I'd love to hear how it goes.\n\nWhat's in the repoEverything is MIT licensed: the firmware, the browser renderer, the native\n\ntest harness, the recorded swing fixtures, and the design docs with the\n\nfull reasoning — including the approaches that didn't survive.\n\n[Read more](javascript:void(0))", "url": "https://wpnews.pro/news/cardminton-playing-badminton-with-a-cardputer-as-the-racket", "canonical_source": "https://www.hackster.io/zhangpengfan6/cardminton-playing-badminton-with-a-cardputer-as-the-racket-1ed594", "published_at": "2026-06-13 06:47:43+00:00", "updated_at": "2026-06-17 16:55:32.785279+00:00", "lang": "en", "topics": ["ai-products", "ai-tools", "developer-tools"], "entities": ["M5Stack", "Cardputer", "Nintendo", "Wii Sports"], "alternates": {"html": "https://wpnews.pro/news/cardminton-playing-badminton-with-a-cardputer-as-the-racket", "markdown": "https://wpnews.pro/news/cardminton-playing-badminton-with-a-cardputer-as-the-racket.md", "text": "https://wpnews.pro/news/cardminton-playing-badminton-with-a-cardputer-as-the-racket.txt", "jsonld": "https://wpnews.pro/news/cardminton-playing-badminton-with-a-cardputer-as-the-racket.jsonld"}}