X-Input (Xbox 360) controller example

Presents the ESP32-S3 as a wired Xbox 360 controller over the native USB-OTG peripheral, using espp::UsbDevice’s X-Input function. A PC’s XUSB driver binds it, so it shows up as an Xbox 360 controller in Windows’ “Set up USB game controllers” (joy.cpl) and any X-Input game, or under Linux xpad.

The demo sweeps the sticks/triggers in a circle and steps the face buttons A/B/X/Y one at a time each second, so you can see live input, and logs any rumble / LED reports the host sends back. The console/logs go to UART0 (with USB-Serial-JTAG as an early-boot secondary): on the ESP32-S3 the USB-Serial-JTAG controller shares the native USB port’s PHY with USB-OTG, so keeping the console on it would contend with the emulated controller interface and reboot-loop the device. Use a UART / USB-UART adapter on UART0 for idf.py monitor.

Build & flash

X-Input is served by a custom TinyUSB application class driver built into the usb_device component, so it needs no built-in USB class enabled — the example’s sdkconfig.defaults disables CDC/vendor/HID entirely. Flash it to an ESP32-S3 and plug the native USB-OTG port into a PC:

idf.py -p <PORT> flash monitor   # console is on UART0 (USB-UART adapter)

Identity (emulation only)

The device enumerates with Microsoft’s Xbox 360 VID/PID (0x045E:0x028E) and device class 0xFF/0xFF/0xFF — that identity is what makes the host’s XUSB driver bind it. These are Microsoft’s identifiers, for emulation / testing of your own device only; a shipped product must not enumerate under them.

Using it in your own code

espp::UsbDevice::Config cfg;
espp::UsbDevice::XInputFunction xinput;      // default VID/PID = Xbox 360 wired
xinput.on_rumble = [](std::span<const uint8_t> data) { /* drive motors / LEDs */ };
cfg.xinput = xinput;                          // X-Input as the ONLY function
espp::UsbDevice usb(cfg);
std::error_code ec;
usb.initialize(ec);

espp::xinput::GamepadState state;
state.set(espp::xinput::Button::A, true);
state.lx = 20000;                             // left stick X
usb.update_xinput_state(state);                    // send a 20-byte input report

See components/usb_device/include/xinput.hpp for the GamepadState / Button API and the report layout, and the component README for the constraints (X-Input must be the only enabled function).