Files
2026-06-07 21:14:44 -04:00

9.4 KiB
Raw Permalink Blame History

Adding Other USB HID Controllers

This guide provides the baseline for adding support for controllers other than the 8BitDo in Nintendo Switch Pro mode. Everything below is derived from what was confirmed working on PS5.


Architecture Overview

The payload has two separate paths:

USB HID Controller  →  gc_main USB thread  →  ScePadData  →  scePadVirtualDeviceInsertData
                                               (parse)              (VDI inject)

To add a new controller you only need to:

  1. Add VID:PID detection in ugen_find_target()
  2. Add an initialization sequence in usb_hid_thread()
  3. Add a parser function that converts the controller's HID report to ScePadData

Step 1: Identify the Controller

Plug the controller into the PS5 USB port and watch klog:

ugen2.2: <ManufacturerName, ProductName>(VID=0xXXXX PID=0xXXXX) at usbus2

Also check:

  • Which ugen path it appears on (/dev/ugen2.2, /dev/ugen1.2, etc.)
  • What kernel driver claims it (usb_hid0, usb_hid1, etc.)

Add the VID:PID to gc_main.c:

#define VID_MYCTLR  0xXXXXu
#define PID_MYCTLR  0xXXXXu

And add detection to ugen_find_target():

if ((di.udi_vendorNo == VID_MYCTLR && di.udi_productNo == PID_MYCTLR)) {
    if (out_vid) *out_vid = di.udi_vendorNo;
    if (out_pid) *out_pid = di.udi_productNo;
    if (out_path) *out_path = UGEN_PATHS[i];
    ok = 1;
}

Step 2: Find the Endpoints

After USB_IFACE_DRIVER_DETACH and USB_FS_INIT, try opening common HID endpoints:

// Standard HID: IN=0x81, OUT=0x01 (or 0x02 for some — check your device)
USB_FS_OPEN(ep_no=0x81, ep_index=0, max_bufsize=64)   // IN
USB_FS_OPEN(ep_no=0x01, ep_index=1, max_bufsize=64)   // OUT (try 0x02 if this fails)

Log max_packet_length from the open result — it tells you the expected HID report size.

Common endpoint configurations:

Controller Family IN OUT Report Size
Nintendo Pro / 8BitDo Nintendo mode 0x81 0x02 64 bytes
Xbox 360 (wired) 0x81 0x02 20 bytes (IN), 8 bytes (OUT)
Xbox One / Series (wired) 0x81 0x02 18 bytes (IN)
Generic HID gamepad 0x81 0x01 varies

Step 3: Determine Init Sequence

Native HID Controllers (no init needed)

Most generic HID gamepads (XInput-incompatible mode, standard HID descriptor) start sending reports immediately after you open the IN endpoint — no OUT commands needed.

// No init: just open IN and start reading
USB_FS_OPEN(ep_no=0x81, ep_index=0);
// → start reading, data comes immediately

Check if USB_FS_COMPLETE returns data within 500ms with no OUT sent. If yes: native HID, skip all init.

Xbox 360 (Wired)

Xbox 360 wired controller uses XInput protocol. It requires an init packet on the OUT endpoint before it sends input reports:

// No handshake needed — just send one OUT "enable" packet
uint8_t enable[] = {0x01, 0x03, 0x0E};  // XInput enable command (3 bytes)
send_out(fd, &eps[1], enable, sizeof(enable));
// Controller then sends 0x00 input reports at ~125Hz

Xbox 360 has no USB mode switching — no handshake like Nintendo. See report format below.

Xbox One / Series (Wired)

Xbox One protocol (XInput HID variant) requires:

// Enable input reports
uint8_t enable[] = {0x05, 0x20, 0x00, 0x01, 0x00};
send_out(fd, &eps[1], enable, sizeof(enable));
// Optionally: rumble keepalive every 5s (or controller disconnects from idle)

Generic / Unknown

  1. Open IN endpoint (no OUT init)
  2. Read for 2 seconds
  3. If no data: try sending [0x00] or [0x01] on OUT endpoint
  4. Capture and log whatever comes in — inspect the HID report manually

Step 4: Parse the HID Report

Identify Report Structure

Log the raw bytes while pressing each button individually:

gp_log("PKT: %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x\n",
       buf[0],buf[1],buf[2],buf[3],buf[4],buf[5],buf[6],buf[7],
       buf[8],buf[9],buf[10],buf[11]);

Press each button, record which bit changes. Common patterns:

Buttons: one byte per group, each button = 1 bit
Sticks:  one byte per axis (0–255 range, center=127–128)
         OR two bytes per axis (little-endian 16-bit, center=~32767)
Triggers:one byte per trigger (0–255)
         OR in button byte (binary on/off)
Dpad:    one nibble (0-8 hat switch) OR four bits in a button byte

Xbox 360 Report Format (20 bytes, report ID = 0x00)

Byte  0: 0x00       — report ID
Byte  1: 0x14       — length (20)
Byte  2: buttons lo — A B X Y LB RB Back Start (bits 4-7: 0000)
Byte  3: buttons hi — LS RS Guide (bits 0-2)
Byte  4: left trigger  (0-255)
Byte  5: right trigger (0-255)
Byte 6-7:  left stick X  (int16 LE, center=0, range -32768..32767)
Byte 8-9:  left stick Y  (int16 LE, center=0, inverted: up=positive)
Byte 10-11: right stick X
Byte 12-13: right stick Y
Byte 14-19: (padding)

Dpad is in buf[2] bits 0-3:

bit 0: Up, bit 1: Down, bit 2: Left, bit 3: Right

Xbox One Report Format (18 bytes, report ID = 0x20)

Byte  0: 0x20       — report ID
Byte  1: 0x00       — sequence
Byte  2: buttons lo — A B X Y LB RB View Menu (bits 0-7)
Byte  3: buttons hi — LS RS (bits 0-1), dpad (bits 2-5)
Byte  4: left trigger  (0-255, maps to 0-1023 internally)
Byte  5: right trigger (0-255)
Byte 6-7:  left stick X  (int16 LE, center=0)
Byte 8-9:  left stick Y  (int16 LE)
Byte 10-11: right stick X
Byte 12-13: right stick Y
Byte 14-17: (varies by firmware)

Step 5: Map to ScePadData

Add a parse function in gc_main.c following this pattern:

static void parse_xbox360(const uint8_t *b, ScePadData *o) {
    uint8_t bl = b[2], bh = b[3];
    uint8_t lt = b[4], rt = b[5];
    int16_t lx = (int16_t)(b[6] | (b[7] << 8));
    int16_t ly = (int16_t)(b[8] | (b[9] << 8));
    int16_t rx = (int16_t)(b[10] | (b[11] << 8));
    int16_t ry = (int16_t)(b[12] | (b[13] << 8));

    uint32_t btn = 0;
    /* Face buttons */
    if (bl & 0x10) btn |= SCE_PAD_BUTTON_CROSS;     // A
    if (bl & 0x20) btn |= SCE_PAD_BUTTON_CIRCLE;    // B
    if (bl & 0x40) btn |= SCE_PAD_BUTTON_SQUARE;    // X
    if (bl & 0x80) btn |= SCE_PAD_BUTTON_TRIANGLE;  // Y
    /* Shoulder */
    if (bh & 0x01) btn |= SCE_PAD_BUTTON_L1;        // LB
    if (bh & 0x02) btn |= SCE_PAD_BUTTON_R1;        // RB
    /* Triggers (analog) */
    o->analogButtons.l2 = lt;
    o->analogButtons.r2 = rt;
    if (lt > 128) btn |= SCE_PAD_BUTTON_L2;
    if (rt > 128) btn |= SCE_PAD_BUTTON_R2;
    /* Sticks */
    if (bh & 0x20) btn |= SCE_PAD_BUTTON_L3;
    if (bh & 0x40) btn |= SCE_PAD_BUTTON_R3;
    /* Menu */
    if (bl & 0x04) btn |= SCE_PAD_BUTTON_CREATE;    // Back
    if (bl & 0x08) btn |= SCE_PAD_BUTTON_OPTIONS;   // Start
    /* Dpad */
    if (bl & 0x01) btn |= SCE_PAD_BUTTON_UP;
    if (bl & 0x02) btn |= SCE_PAD_BUTTON_DOWN;
    if (bl & 0x04) btn |= SCE_PAD_BUTTON_LEFT;
    if (bl & 0x08) btn |= SCE_PAD_BUTTON_RIGHT;

    o->buttons = btn;
    /* Map int16 (-32768..32767) → uint8 (0..255), center=127 */
    o->leftStick.x  = (uint8_t)((lx + 32768) >> 8);
    o->leftStick.y  = (uint8_t)(255 - ((ly + 32768) >> 8));  // Y axis inverted
    o->rightStick.x = (uint8_t)((rx + 32768) >> 8);
    o->rightStick.y = (uint8_t)(255 - ((ry + 32768) >> 8));
    o->connected = 1;
    o->quat.w = 1.0f;
}

Then in the main read loop, add a branch:

} else if (pid == PID_XBOX360 && rid == 0x00 && len >= 14) {
    parse_xbox360(buf, &pad);
    if (g_vdi_ready) inject_pad(&pad);

Step 6: Add Init to USB Thread

In usb_hid_thread(), after the two-pass setup, add a branch for your controller:

if (pid == PID_SWITCH && out_opened) {
    // 8BitDo Nintendo mode — full handshake (existing)
    ...
} else if (pid == PID_XBOX360 && out_opened) {
    uint8_t enable[] = {0x01, 0x03, 0x0E};
    usb_fs_send_out_report(fd, &eps[1], enable, sizeof(enable), "xbox360_enable");
    hs_state = HS_STREAMING;  // Xbox 360 has no handshake
} else {
    // Generic HID: no init, just start reading
    hs_state = HS_STREAMING;
}

Button Mapping Reference

All PS5 SCE_PAD_BUTTON_* values used in gc_main.c:

SCE_PAD_BUTTON_L3        = 0x00000002
SCE_PAD_BUTTON_R3        = 0x00000004
SCE_PAD_BUTTON_OPTIONS   = 0x00000008   // Options / Start / Menu
SCE_PAD_BUTTON_UP        = 0x00000010
SCE_PAD_BUTTON_RIGHT     = 0x00000020
SCE_PAD_BUTTON_DOWN      = 0x00000040
SCE_PAD_BUTTON_LEFT      = 0x00000080
SCE_PAD_BUTTON_L2        = 0x00000100
SCE_PAD_BUTTON_R2        = 0x00000200
SCE_PAD_BUTTON_L1        = 0x00000400
SCE_PAD_BUTTON_R1        = 0x00000800
SCE_PAD_BUTTON_TRIANGLE  = 0x00001000
SCE_PAD_BUTTON_CIRCLE    = 0x00002000
SCE_PAD_BUTTON_CROSS     = 0x00004000
SCE_PAD_BUTTON_SQUARE    = 0x00008000
SCE_PAD_BUTTON_CREATE    = 0x00010000   // Share / Back / View
SCE_PAD_BUTTON_PS        = 0x00010000   // PS button / Guide / Home
SCE_PAD_BUTTON_TOUCH_PAD = 0x00100000

ScePadData stick fields: uint8_t x, y — range 0–255, center = 127–128. ScePadData trigger fields: uint8_t l2, r2 — range 0–255.


Checklist for a New Controller

  • VID:PID added to constants and ugen_find_target()
  • Controller type notification added
  • Init sequence determined (native / Xbox / custom handshake)
  • IN endpoint confirmed working (max_packet_length logged)
  • Raw bytes logged per button press — report format mapped
  • Parser function written and tested
  • Read loop handles new pid and rid correctly
  • Reconnect path tested (unplug and replug while payload running)