Turn a phone or tablet into a low-latency virtual gamepad for Batocera/Linux using a Flask + Socket.IO web app and Linux uinput.
WebController serves a mobile-friendly controller UI (sticks, d-pad, face buttons, triggers, system buttons) and forwards real-time input to virtual gamepads created with evdev.
Each connected client gets an isolated virtual controller, enabling local multiplayer over your LAN.
- File:
preview.png - Recommended screenshot content:
- Landscape mobile view of the controller with both joysticks visible.
- Settings panel open showing template management.
- Optional overlay showing connected player count.
- Real-time Socket.IO input transport.
- Virtual Xbox 360-style gamepad output via
uinput. - Multi-player support (configurable max players).
- Input smoothing + deadzone filtering for sticks/triggers.
- D-pad support (hat + button events).
- Layout editor with template save/import/export.
- Static vs dynamic left joystick mode.
- PWA manifest and mobile splash/icon assets.
- Backend: Python, Flask, Flask-SocketIO, evdev-binary
- Frontend: HTML, CSS, vanilla JavaScript, Web Components (
virtual-joystick) - Runtime: Linux
uinputkernel module
- Linux host (Batocera-compatible)
- Python 3.10+ (tested with modern Python 3)
uinputavailable (modprobe uinput)
- Clone the repository:
git clone https://github.com/arbaz93/joycon.git cd joycon - Create and activate a virtual environment:
python3 -m venv .venv source .venv/bin/activate - Install dependencies:
pip install -r requirements.txt
- Ensure
uinputis available:sudo modprobe uinput sudo chmod 666 /dev/uinput
You can configure runtime behavior with environment variables:
HOST=0.0.0.0
PORT=5000
LOG_LEVEL=INFO
CORS_ALLOWED_ORIGINS=*
MAX_PLAYERS=4
PROCESS_HZ=120
QUEUE_MAXLEN=512
MAX_EVENTS_PER_TICK=256
STICK_DEADZONE=0.15
TRIGGER_DEADZONE=0.02
STICK_SMOOTH_ALPHA=0.35
TRIGGER_SMOOTH_ALPHA=0.45
STICK_MAX_STEP=6000
TRIGGER_MAX_STEP=48
PAD_NAME=Microsoft X-Box 360 pad- Start the server:
python3 server.py
- Open from a mobile device on the same network:
http://<server-ip>:5000 - Rotate phone to landscape and start controlling.
Use autostart-webcontroller.sh to run at boot or from Batocera startup hooks.
GET /
Serves the controller shell page.
button:{ "button": "A|B|X|Y|LB|RB|START|SELECT|HOME|L3|R3", "state": 0|1 }joystick:{ "stick": "left|right", "x": -1..1, "y": -1..1 }trigger:{ "trigger": "LT|RT", "value": 0..1 }dpad:{ "x": -1|0|1, "y": -1|0|1 }
.
├── server.py
├── autostart-webcontroller.sh
├── requirements.txt
├── preview.png
├── templates/
│ └── index.html
└── static/
├── content.html
├── manifest.json
├── default-template.json
├── css/
│ ├── styles.css
│ └── joystick.css
├── javascript/
│ ├── index.js
│ ├── controller.js
│ └── virtual-joystick.js
└── icons/
- Browser UI emits controller events through Socket.IO.
- Server queues events per connected client.
- Background processing loop applies deadzones/smoothing and resolves state.
- Changed values are written to
uinputvirtual devices. - Emulation frontends detect those as standard controllers.
flowchart LR
A[Mobile Browser UI] -->|Socket.IO events| B[Flask-SocketIO Server]
B --> C[Per-client Event Queue]
C --> D[Processing Loop<br/>deadzone + smoothing]
D --> E[evdev UInput]
E --> F[Virtual Gamepad Device]
F --> G[Batocera / Emulator]
- Couch multiplayer without extra physical gamepads.
- Quick input testing for emulator configs.
- Lightweight remote controller for kiosk/arcade setups.
- No native mobile app required.
- Works over LAN with standard browser support.
- Fine-grained layout customization and reusable templates.
- Split
controller.jsinto feature modules. - Add authentication / pairing flow.
- Add WebSocket reconnect UX and player slot UI.
- Add telemetry and optional diagnostics page.
- Add automated tests + CI pipeline.
- Fork repository and create a feature branch.
- Keep changes focused and document behavioral changes.
- Run lint/tests (once test suite is added).
- Open a PR with screenshots for UI changes.
- Exposing this service to untrusted networks is not recommended.
- Restrict
CORS_ALLOWED_ORIGINSin production. - Keep
uinputpermission changes scoped to trusted host users.
This project is licensed under the MIT License. See LICENSE.
