A whack-a-mole–style reaction-time test bed for a robotic finger.
Open the web app on a touch device (e.g. an iPad). It pairs with a Python WebSocket server on another computer that drives the robot. A red dot appears at a random location after a random delay; tapping it reports the reaction time in milliseconds back over the socket.
- The web app connects to the Python server over WebSocket.
- On
Start, it picks a random pixel location inside the play area, waits a random amount of time (600–2400 ms), then renders a target. - On spawn, it sends
{ type: "spawn", target: { x, y }, ... }so the robot can move. - On tap (
pointerdown), it measures the elapsed time withperformance.now()and sends{ type: "hit", reactionMs, tap, ... }. - If the dot isn't tapped within 5 s it reports
{ type: "miss" }and schedules another.
All coordinates are in CSS pixels in the browser viewport (top-left
origin) on the paired device. The hello and spawn messages also
include viewport, screen, playArea, and devicePixelRatio so the
robot side can calibrate.
Run the web app and the Python server on the same machine the robot is
attached to. The iPad joins the same Wi-Fi and opens both over plain
HTTP / ws://. No TLS, no tunnels, no mixed-content issues.
# On the robot machine (Linux):
git clone git@github.com:davidavidavidavi/whackmole.git
cd whackmole
# 1. Web app
npm install
npm run dev # http://0.0.0.0:4000
# 2. Python server (in a second terminal)
cd python
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python server.py # ws://0.0.0.0:8765Find the robot machine's IP:
hostname -I # e.g. 192.168.10.42On the iPad, open:
http://192.168.10.42:4000
The WebSocket URL is pre-filled to ws://<that-same-host>:8765, so just
tap Connect.
The Python terminal prints each event:
listening on ws://0.0.0.0:8765
[connect] 192.168.10.20:53412
[paired] viewport=1024x768 screen=1024x768 dpr=2
[spawn] id=k2x9a3qr x=512 y=384 size=84 delay=1700ms
[hit] id=k2x9a3qr reaction=312ms tap=(514, 380)
Replace the placeholder logic in python/server.py with your motion
controller. Spawn messages give you the target's (x, y) in viewport
pixels on the iPad.
The web app is a vanilla Next.js project — Vercel auto-detects it.
- Push the repo to GitHub (already done).
- Go to https://vercel.com/new and import the repo.
- Accept the defaults and deploy. You'll get an
https://*.vercel.appURL that works on any iPad.
.vercelignore keeps the python/ folder out of the build upload.
A Vercel deploy is served over HTTPS. From an HTTPS page, browsers
block plain ws:// WebSocket connections — the connection will
fail silently except for the inline warning the app shows. You need
wss:// (TLS) on the Python side.
Easiest options to put TLS in front of python/server.py:
- Tailscale Funnel — if both devices are on Tailscale.
Then use
tailscale serve --bg --tls-terminated-tcp=443 tcp://localhost:8765 tailscale funnel 443 onwss://<your-tailnet-name>.ts.net:443in the app. - Cloudflare Tunnel — no static IP needed.
Use the
cloudflared tunnel --url http://localhost:8765wss://*.trycloudflare.comURL it prints. - Caddy in front, with a real domain pointing at the machine.
For local development the dev server is plain HTTP, so plain ws://
works fine — no proxy needed.
Client → server:
The server doesn't need to send anything back, though the connection stays open so it can if you extend the protocol.
pointerdownis used so the timer captures touch start, not click (avoids the synthetic-click delay).- Pinch zoom and tap-zoom are disabled via the viewport meta and
touch-action: manipulation. - For the cleanest fullscreen experience, add the page to the home screen.