linux-joystick is a Linux C++ server that:
- Creates a virtual gamepad with
/dev/uinput. - Receives controller state over UDP.
- Applies that state to the virtual gamepad.
- Exposes a UDP discovery endpoint so clients can find the server.
The executable built by this repo is virtual_remote_server.
- Binary controller protocol (
ControllerPacketV1, 36 bytes), not text commands. - UDP control receiver on port
9000. - Pair-lock to first sender IP until timeout.
- Input watchdog:
-
200 ms silence: send neutral state.
-
2000 ms silence: unlock paired IP and neutral again.
-
- UDP discovery service on port
9002with request/response structs. - Stable
server_idgeneration from/etc/machine-idhash (fallback string if missing). - Per-IP discovery response rate limiting (~1 response / 150 ms).
- Virtual gamepad axes/buttons mapped to Linux input event codes.
CMakeLists.txt: builds single targetvirtual_remote_serverwith C++17.src/server_main.cpp: process entry point, starts gamepad + UDP control + discovery service.src/protocol.h: packed binary control packet definition and button bit assignments.src/controller_engine.h/.cpp: packet validation, pairing lock, watchdog timeout, and input dispatch.src/gamepad_mapping.h: mapping from protocol controls to LinuxEV_KEY/EV_ABScodes.src/virtual_gamepad.h/.cpp:/dev/uinputsetup, device creation/destruction, event emission.src/udp_receiver.h/.cpp: UDP socket bind + datagram receive wrapper for control traffic.src/discovery_protocol.h: packed binary discovery request/response structs and flags.src/discovery_service.h/.cpp: UDP discovery socket thread, validation, status response building.include/: currently empty.
9000/udp: control packets (ControllerPacketV1).9002/udp: discovery (DiscoverReqV1->DiscoverRespV1).
ControllerPacketV1 is #pragma pack(push, 1) and must be exactly 36 bytes.
Fields:
magic(uint32_t): must equal0x4F494E55(UNIO_MAGIC).version(uint16_t): must equal1.size(uint16_t): must equalsizeof(ControllerPacketV1)(36).seq(uint32_t): sequence number.ts_us(uint64_t): timestamp in microseconds (client-provided).lx, ly, rx, ry(int16_t): stick axes in-32768..32767.l2, r2(uint8_t): trigger values in0..255.dpad_x, dpad_y(int8_t): each in-1, 0, 1.buttons(uint32_t): bitmask usingButtonBitsenum.
Button bits:
- 0
A - 1
B - 2
X - 3
Y - 4
L1 - 5
R1 - 6
L3 - 7
R3 - 8
SELECT - 9
START - 10
HOME(defined in protocol, currently not applied inControllerEngine)
ControllerEngine::processPacket:
- Applies watchdog logic based on time since last valid packet.
- Rejects packets shorter than 36 bytes.
- Copies packet bytes and validates
magic/version/size. - Pair-locks to first sender IP after unlocked state.
- Rejects packets from other IPs while locked.
- Sends all axes and button states to virtual gamepad.
- Issues
sync()after each accepted packet.
Neutral state sets:
- All sticks to
0. - Triggers to
0. - D-pad to
0. - Buttons
A/B/X/Y/L1/R1/L3/R3/SELECT/STARTto released.
Device path:
/dev/uinputopened withO_WRONLY | O_NONBLOCK.
Enabled event types:
EV_KEYEV_ABS
Buttons enabled:
GP_A,GP_B,GP_X,GP_YGP_L1,GP_R1GP_START,GP_SELECTGP_L3,GP_R3,GP_HOME
Axes configured:
GP_LX,GP_LY,GP_RX,GP_RY:-32768..32767GP_DPAD_X,GP_DPAD_Y:-1..1GP_L2,GP_R2:0..255
uinput identity:
- Name:
Virtual Remote Gamepad - Bus:
BUS_USB - Vendor/Product:
0x1234 / 0x5678
Magic/version:
kDiscoveryMagic = 0x53444A4C(LJDSin little-endian bytes)kDiscoveryVersion = 1
Request (DiscoverReqV1):
magic,version,msg_type=DiscoverReq,reserved,nonce
Response (DiscoverRespV1):
magic,version,msg_type=DiscoverResp,reservednonce(echoed from request)server_idcontrol_portreserved_port(currently0, reserved for protocol compatibility)proto_vername_lenflags- followed by
namebytes (not null-terminated, max 64)
Flags currently used:
kFlagPairedLockedset when controller engine is locked.
cmake -S . -B build
cmake --build build -jsudo ./build/virtual_remote_serversudo (or equivalent permissions) is usually required for /dev/uinput.
On successful start, server prints:
Server running:UDP control : 9000UDP discovery : 9002
On first accepted controller sender, stderr logs lock event:
[lock] locking to ip=<numeric_ipv4_value>








