diff --git a/design/steam-controller-deck-support.md b/design/steam-controller-deck-support.md index 378c825..d3b974d 100644 --- a/design/steam-controller-deck-support.md +++ b/design/steam-controller-deck-support.md @@ -758,3 +758,33 @@ HID feature/output reports, which `f_hid` can't but **`raw_gadget` can** (usersp control transfer, exactly like the UHID path). Next: a `raw_gadget` userspace emulator of the 3-interface Deck (controller on interface 2) that answers the serial/attribute/settings feature reports + streams the 64-byte state report — then re-test hid-steam gamepad evdev + Steam promotion. + +### Gadget path SUCCESS — raw_gadget Deck gets full Steam Input recognition (2026-06-29) + +The `f_hid` zombie was a feature-report problem, and `raw_gadget` (userspace handles every control +transfer) solves it. `packaging/linux/steam-deck-gadget/deck_raw_gadget.c` presents the real +3-interface Deck (descriptors captured verbatim from a physical Deck, controller on interface 2) and +answers the HID feature reports hid-steam/Steam need. Live on the Deck: + +``` +hid-steam ... Steam Controller 'PFDECK000' connected (serial READ — not XXXXXXXXXX) +input: Steam Deck / Steam Deck Motion Sensors (kernel gamepad + IMU evdevs) +controller.txt: Interface: 2 ... device opened for index 14 ... reserving XInput slot 1 +input: Microsoft X-Box 360 pad 1 (Steam Input's XInput output — PROMOTED) +``` + +Stable (1 connect, 0 disconnects, no zombie). The kernel `"Steam Deck"` evdev is then grabbed by +Steam Input, which exposes its own X-Box 360 pad — a real Deck's exact behaviour. **This is the first +time a virtual Steam Deck has been fully promoted by Steam Input** (UHID can't; the interface-2 wall +is climbed). The hard part — recognition — is done. + +Implementation gotchas (see the packaging README): `struct usb_endpoint_descriptor` is 9 bytes but +the wire descriptor needs 7; no-data OUT controls are acked with a zero-length `EP0_READ` not +`EP0_WRITE` (else `error -110`); the input streamer must not start until after SET_CONFIGURATION is +acked. Scope: SteamOS-host only (needs `dummy_hcd` + `raw_gadget`, which SteamOS ships; a generic +Linux host would have to build them). + +**Remaining:** feed real client state through the interface-2 endpoint (the `steam_proto` serializer +already produces correct Deck reports — wire it to the gadget's stream), and wrap this as a host +gamepad backend (a `raw_gadget` alternative to the UHID `SteamDeckPad`). Then the streamed Deck/SC +client reaches the host's games as a true Deck through Steam Input. diff --git a/packaging/linux/steam-deck-gadget/README.md b/packaging/linux/steam-deck-gadget/README.md new file mode 100644 index 0000000..1f98eb6 --- /dev/null +++ b/packaging/linux/steam-deck-gadget/README.md @@ -0,0 +1,64 @@ +# Virtual Steam Deck via USB gadget — true Steam Input recognition + +**Proven on a real Steam Deck (SteamOS 3.8.11), 2026-06-29.** A `raw_gadget` userspace emulator of a +real 3-interface USB Steam Deck (`28DE:1205`) — mouse = interface 0, keyboard = 1, **controller = +interface 2** — bound to a `dummy_hcd` loopback UDC, so the host's own Steam sees a genuine +interface-2 Deck and **promotes it through Steam Input** (XInput pad emission, glyphs, bindings). + +## Why this exists (the interface-2 wall) + +A virtual Deck created via **UHID** (the `inject/proto/steam_proto.rs` / `steam_controller.rs` path) +binds the kernel `hid-steam` driver, but **Steam Input will not manage it**: Steam filters the Deck's +controller to USB **interface 2**, and a UHID device has no USB interface number (`Interface: -1` in +Steam's `controller.txt`), so Steam enumerates it but never promotes it. A single-interface DualSense +is accepted at `-1` (no ambiguity), but the multi-interface Deck specifically needs interface 2. See +`design/steam-controller-deck-support.md` §11. + +A real multi-interface USB device with the controller on interface 2 requires a **USB gadget**. +SteamOS ships every piece (`CONFIG_USB_DUMMY_HCD=m`, `CONFIG_USB_RAW_GADGET=m`, +`CONFIG_USB_CONFIGFS_F_HID=y`), so this runs on a Deck with no module-building. + +## What's here + +- **`deck_raw_gadget.c`** — the working emulator. Presents the 3-interface Deck with descriptors + captured verbatim from a physical Deck (incl. the real 38-byte controller report descriptor), and + — crucially — answers **every** control transfer, including the HID feature reports (`f_hid` can't, + so it produced a "zombie controller" in Steam). Streams the 64-byte state report on the interface-2 + interrupt-IN endpoint. Build static (the Deck has no compiler): + ```sh + gcc -O2 -static -pthread -o deck_raw_gadget deck_raw_gadget.c + ``` + Run as root with `dummy_hcd` + `raw_gadget` loaded: `./deck_raw_gadget [seconds]`. +- **`configfs_gadget_up.sh` / `_down.sh`** — the simpler **configfs `f_hid`** variant. It proves the + structure (interface 2 → `hid-steam` binds → Steam *opens* it + *reserves an XInput slot*) but + `f_hid` cannot serve HID feature reports, so Steam can't read controller details and drops it as a + zombie. Kept as the minimal reproducer of the interface-2 result. + +## Result (raw_gadget, live) + +``` +hid-steam ... Steam Controller 'PFDECK000' connected +input: Steam Deck / Steam Deck Motion Sensors (kernel gamepad + IMU evdevs) +controller.txt: Interface: 2 ... device opened for index 14 ... reserving XInput slot 1 +input: Microsoft X-Box 360 pad 1 (Steam Input's XInput output — promoted) +``` +Stable (1 connect, 0 disconnects), no zombie. The kernel `"Steam Deck"` evdev is then grabbed by +Steam Input, which exposes its own X-Box 360 pad — exactly a real Deck's behaviour. + +## Key implementation gotchas (all real, all cost time) + +- `struct usb_endpoint_descriptor` (ch9.h) is **9 bytes** (audio `bRefresh`/`bSynchAddress`); the wire + descriptor needs **7** — use a packed 7-byte struct in the config blob or the kernel mis-parses it. +- raw_gadget EP0: a **no-data OUT** control (`SET_CONFIGURATION`, `SET_INTERFACE`, `SET_IDLE`, + `SET_PROTOCOL`) is completed with a zero-length **`EP0_READ`**, not `EP0_WRITE` (using write → + `EBUSY`/`can't set config error -110`). IN controls (`GET_*`) use `EP0_WRITE`. +- Don't start the input streamer until after `SET_CONFIGURATION` is fully acked, or its blocking + `EP_WRITE` starves the control path. +- `dummy_hcd` + `raw_gadget` must both be loaded and `/dev/raw-gadget` present before launch. + +## Status / next + +Recognition is proven. Remaining: feed real client state (the `steam_proto` serializer already +produces correct Deck reports) through the interface-2 endpoint, and wrap this as a host gamepad +backend (a `raw_gadget` alternative to the UHID `SteamDeckPad`) — SteamOS-host only, since it needs +`dummy_hcd` + `raw_gadget`. diff --git a/packaging/linux/steam-deck-gadget/configfs_gadget_down.sh b/packaging/linux/steam-deck-gadget/configfs_gadget_down.sh new file mode 100644 index 0000000..f3ef1dc --- /dev/null +++ b/packaging/linux/steam-deck-gadget/configfs_gadget_down.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Tear down the PoC virtual Deck gadget. +G=/sys/kernel/config/usb_gadget/pfdeck +[ -d "$G" ] || { echo "no gadget"; exit 0; } +echo "" > "$G/UDC" 2>/dev/null || true +for l in "$G"/configs/c.1/hid.usb*; do [ -e "$l" ] && rm -f "$l"; done +rmdir "$G"/configs/c.1/strings/0x409 2>/dev/null || true +rmdir "$G"/configs/c.1 2>/dev/null || true +rmdir "$G"/functions/hid.usb* 2>/dev/null || true +rmdir "$G"/strings/0x409 2>/dev/null || true +rmdir "$G" 2>/dev/null || true +echo "gadget torn down ($(ls /sys/kernel/config/usb_gadget/ 2>/dev/null | wc -l) gadgets remain)" diff --git a/packaging/linux/steam-deck-gadget/configfs_gadget_up.sh b/packaging/linux/steam-deck-gadget/configfs_gadget_up.sh new file mode 100644 index 0000000..f3ab12e --- /dev/null +++ b/packaging/linux/steam-deck-gadget/configfs_gadget_up.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# PoC: stand up a REAL 3-interface USB Steam Deck (28DE:1205) on a dummy_hcd loopback UDC, with the +# controller on **interface 2** (kbd=0, mouse=1) — the structure Steam's controller driver filters +# for. Run as root on the Deck (which ships dummy_hcd + configfs f_hid). Then we check: does hid-steam +# bind interface 2, and does the Deck's own Steam promote it (controller.txt "Interface: 2")? +set -e +G=/sys/kernel/config/usb_gadget/pfdeck + +echo "== modprobe dummy_hcd + libcomposite ==" +modprobe dummy_hcd +modprobe libcomposite +UDC=$(ls /sys/class/udc | grep -i dummy | head -1) +echo "dummy UDC: ${UDC:-}" +[ -n "$UDC" ] || { echo "no dummy UDC — abort"; exit 1; } + +# Tear down a prior instance if present. +if [ -d "$G" ]; then + echo "" > "$G/UDC" 2>/dev/null || true + for l in "$G"/configs/c.1/hid.usb*; do [ -e "$l" ] && rm -f "$l"; done + rmdir "$G"/configs/c.1/strings/0x409 2>/dev/null || true + rmdir "$G"/configs/c.1 2>/dev/null || true + rmdir "$G"/functions/hid.usb* 2>/dev/null || true + rmdir "$G"/strings/0x409 2>/dev/null || true + rmdir "$G" 2>/dev/null || true +fi + +echo "== build gadget $G ==" +mkdir -p "$G"; cd "$G" +echo 0x28de > idVendor +echo 0x1205 > idProduct +echo 0x0110 > bcdDevice +echo 0x0200 > bcdUSB +mkdir -p strings/0x409 +echo "Valve Software" > strings/0x409/manufacturer +echo "Steam Deck Controller" > strings/0x409/product +echo "PFDECK0001" > strings/0x409/serialnumber + +# --- interface 0: boot keyboard --- +mkdir -p functions/hid.usb0 +echo 1 > functions/hid.usb0/protocol +echo 1 > functions/hid.usb0/subclass +echo 8 > functions/hid.usb0/report_length +printf '\x05\x01\x09\x06\xa1\x01\x05\x07\x19\xe0\x29\xe7\x15\x00\x25\x01\x75\x01\x95\x08\x81\x02\x95\x01\x75\x08\x81\x03\x95\x05\x75\x01\x05\x08\x19\x01\x29\x05\x91\x02\x95\x01\x75\x03\x91\x03\x95\x06\x75\x08\x15\x00\x25\x65\x05\x07\x19\x00\x29\x65\x81\x00\xc0' > functions/hid.usb0/report_desc + +# --- interface 1: boot mouse --- +mkdir -p functions/hid.usb1 +echo 2 > functions/hid.usb1/protocol +echo 1 > functions/hid.usb1/subclass +echo 4 > functions/hid.usb1/report_length +printf '\x05\x01\x09\x02\xa1\x01\x09\x01\xa1\x00\x05\x09\x19\x01\x29\x03\x15\x00\x25\x01\x95\x03\x75\x01\x81\x02\x95\x01\x75\x05\x81\x03\x05\x01\x09\x30\x09\x31\x15\x81\x25\x7f\x75\x08\x95\x02\x81\x06\xc0\xc0' > functions/hid.usb1/report_desc + +# --- interface 2: the Steam Deck controller (STEAMDECK_RDESC) --- +mkdir -p functions/hid.usb2 +echo 0 > functions/hid.usb2/protocol +echo 0 > functions/hid.usb2/subclass +echo 64 > functions/hid.usb2/report_length +printf '\x06\x00\xff\x09\x01\xa1\x01\x15\x00\x26\xff\x00\x75\x08\x95\x40\x09\x01\x81\x02\x09\x01\x95\x40\xb1\x02\xc0' > functions/hid.usb2/report_desc + +# --- config, link in order so interface numbers are 0,1,2 --- +mkdir -p configs/c.1/strings/0x409 +echo "Punktfunk virtual Deck" > configs/c.1/strings/0x409/configuration +echo 250 > configs/c.1/MaxPower +ln -s functions/hid.usb0 configs/c.1/ +ln -s functions/hid.usb1 configs/c.1/ +ln -s functions/hid.usb2 configs/c.1/ + +echo "== bind to $UDC ==" +echo "$UDC" > UDC +sleep 2 + +echo ""; echo "===== VERIFY =====" +echo "--- /sys hid devices for 28DE (which interface, which driver) ---" +for d in /sys/bus/hid/devices/*28DE*; do + [ -e "$d" ] || continue + rp=$(readlink -f "$d") + echo " $(basename "$d"): bInterfaceNumber=$(cat "$rp/../bInterfaceNumber" 2>/dev/null) driver=$(basename "$(readlink -f "$d/driver" 2>/dev/null)")" +done +echo "--- hidg char devices (controller = hidg for interface 2) ---"; ls -1 /dev/hidg* 2>/dev/null +echo "--- kernel log (hid-steam bind + Steam Deck evdev) ---" +journalctl -k --since "20 seconds ago" --no-pager 2>/dev/null | grep -iE "steam|28de|1205|hid-generic" | tail -10 +echo "--- /proc input: Steam Deck evdevs created? ---" +grep -c '^N: Name="Steam Deck' /proc/bus/input/devices | sed 's/^/ Steam Deck input nodes: /' +echo "--- lsusb ---"; lsusb -d 28de:1205 2>/dev/null || true +echo "" +echo "Gadget is UP. Feed a neutral controller report with: printf '\\x01\\x00\\x09\\x3c' | dd of=/dev/hidg2 ..." +echo "Tear down with: deck_gadget_down.sh" diff --git a/packaging/linux/steam-deck-gadget/deck_raw_gadget.c b/packaging/linux/steam-deck-gadget/deck_raw_gadget.c new file mode 100644 index 0000000..547fea7 --- /dev/null +++ b/packaging/linux/steam-deck-gadget/deck_raw_gadget.c @@ -0,0 +1,260 @@ +// raw_gadget emulator of a real 3-interface USB Steam Deck (28DE:1205): mouse=iface0, keyboard=iface1, +// controller=iface2 (the structure Steam filters for). Unlike f_hid, raw_gadget lets us answer EVERY +// control transfer — including the HID feature reports hid-steam/Steam need (the serial etc.) — so the +// Deck fully initialises (gamepad evdev) and Steam can read controller details (no "zombie"). +// +// Descriptors captured verbatim from a physical Deck. Build (static, to run on SteamOS): +// gcc -O2 -static -o deck_raw_gadget deck_raw_gadget.c -lpthread +// Run as root on a host with dummy_hcd loaded: ./deck_raw_gadget [seconds] +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ---- raw_gadget UAPI (inlined so we don't depend on the header) ---- */ +#define UDC_NAME_LENGTH_MAX 128 +struct usb_raw_init { __u8 driver_name[UDC_NAME_LENGTH_MAX]; __u8 device_name[UDC_NAME_LENGTH_MAX]; __u8 speed; }; +enum usb_raw_event_type { USB_RAW_EVENT_INVALID, USB_RAW_EVENT_CONNECT, USB_RAW_EVENT_CONTROL }; +struct usb_raw_event { __u32 type; __u32 length; __u8 data[0]; }; +struct usb_raw_ep_io { __u16 ep; __u16 flags; __u32 length; __u8 data[0]; }; +#define USB_RAW_EPS_NUM_MAX 30 +#define USB_RAW_EP_NAME_MAX 16 +struct usb_raw_ep_caps { __u32 type_control:1, type_iso:1, type_bulk:1, type_int:1, dir_in:1, dir_out:1; }; +struct usb_raw_ep_limits { __u16 maxpacket_limit; __u16 max_streams; __u32 reserved; }; +struct usb_raw_ep_info { __u8 name[USB_RAW_EP_NAME_MAX]; __u32 addr; struct usb_raw_ep_caps caps; struct usb_raw_ep_limits limits; }; +struct usb_raw_eps_info { struct usb_raw_ep_info eps[USB_RAW_EPS_NUM_MAX]; }; +#define USB_RAW_IOCTL_INIT _IOW('U', 0, struct usb_raw_init) +#define USB_RAW_IOCTL_RUN _IO('U', 1) +#define USB_RAW_IOCTL_EVENT_FETCH _IOR('U', 2, struct usb_raw_event) +#define USB_RAW_IOCTL_EP0_WRITE _IOW('U', 3, struct usb_raw_ep_io) +#define USB_RAW_IOCTL_EP0_READ _IOWR('U', 4, struct usb_raw_ep_io) +#define USB_RAW_IOCTL_EP_ENABLE _IOW('U', 5, struct usb_endpoint_descriptor) +#define USB_RAW_IOCTL_EP_WRITE _IOW('U', 7, struct usb_raw_ep_io) +#define USB_RAW_IOCTL_CONFIGURE _IO('U', 9) +#define USB_RAW_IOCTL_VBUS_DRAW _IOW('U', 10, __u32) +#define USB_RAW_IOCTL_EPS_INFO _IOR('U', 11, struct usb_raw_eps_info) +#define USB_RAW_IOCTL_EP0_STALL _IO('U', 12) + +/* ---- captured-from-hardware report descriptors ---- */ +static const __u8 RDESC_MOUSE[] = { + 0x05,0x01,0x09,0x02,0xa1,0x01,0x09,0x01,0xa1,0x00,0x05,0x09,0x19,0x01,0x29,0x02, + 0x15,0x00,0x25,0x01,0x75,0x01,0x95,0x02,0x81,0x02,0x75,0x06,0x95,0x01,0x81,0x01, + 0x05,0x01,0x09,0x30,0x09,0x31,0x15,0x81,0x25,0x7f,0x75,0x08,0x95,0x02,0x81,0x06, + 0x95,0x01,0x09,0x38,0x81,0x06,0x05,0x0c,0x0a,0x38,0x02,0x95,0x01,0x81,0x06,0xc0,0xc0 }; +static const __u8 RDESC_KBD[] = { + 0x05,0x01,0x09,0x06,0xa1,0x01,0x05,0x07,0x19,0xe0,0x29,0xe7,0x15,0x00,0x25,0x01, + 0x75,0x01,0x95,0x08,0x81,0x02,0x81,0x01,0x19,0x00,0x29,0x65,0x15,0x00,0x25,0x65, + 0x75,0x08,0x95,0x06,0x81,0x00,0xc0 }; +static const __u8 RDESC_CTRL[] = { // the real Deck controller, interface 2 + 0x06,0xff,0xff,0x09,0x01,0xa1,0x01,0x09,0x02,0x09,0x03,0x15,0x00,0x26,0xff,0x00, + 0x75,0x08,0x95,0x40,0x81,0x02,0x09,0x06,0x09,0x07,0x15,0x00,0x26,0xff,0x00,0x75, + 0x08,0x95,0x40,0xb1,0x02,0xc0 }; + +/* ---- HID descriptor (one per interface, points at the report descriptor length) ---- */ +struct hid_desc { __u8 bLength,bDescriptorType; __u16 bcdHID; __u8 bCountryCode,bNumDescriptors,bReportType; __u16 wReportLength; } __attribute__((packed)); +/* Exact 7-byte endpoint descriptor — `struct usb_endpoint_descriptor` is 9 bytes (audio bRefresh/ + bSynchAddress), which would inject 2 garbage bytes per endpoint into the wire config + mis-parse. */ +struct ep_desc7 { __u8 bLength,bDescriptorType,bEndpointAddress,bmAttributes; __u16 wMaxPacketSize; __u8 bInterval; } __attribute__((packed)); + +/* ---- full config descriptor, assembled to mirror the real Deck (3 HID interfaces) ---- */ +struct config_blob { + struct usb_config_descriptor config; + struct usb_interface_descriptor i0; struct hid_desc h0; struct ep_desc7 e0; + struct usb_interface_descriptor i1; struct hid_desc h1; struct ep_desc7 e1; + struct usb_interface_descriptor i2; struct hid_desc h2; struct ep_desc7 e2; +} __attribute__((packed)); +/* Full 9-byte endpoint descriptors, used only for the EP_ENABLE ioctl. */ +static struct usb_endpoint_descriptor epfull0, epfull1, epfull2; + +static struct usb_device_descriptor dev_desc = { + .bLength = USB_DT_DEVICE_SIZE, .bDescriptorType = USB_DT_DEVICE, .bcdUSB = 0x0200, + .bDeviceClass = 0, .bDeviceSubClass = 0, .bDeviceProtocol = 0, .bMaxPacketSize0 = 64, + .idVendor = 0x28de, .idProduct = 0x1205, .bcdDevice = 0x0300, + .iManufacturer = 1, .iProduct = 2, .iSerialNumber = 3, .bNumConfigurations = 1 }; + +#define HID_DT 0x21 +#define HID_RPT_DT 0x22 +static struct config_blob cfg; +static void build_config(void) { + memset(&cfg, 0, sizeof(cfg)); + cfg.config = (struct usb_config_descriptor){ .bLength = USB_DT_CONFIG_SIZE, .bDescriptorType = USB_DT_CONFIG, + .wTotalLength = sizeof(cfg), .bNumInterfaces = 3, .bConfigurationValue = 1, .iConfiguration = 0, + .bmAttributes = 0x80, .bMaxPower = 250 }; + // iface 0: mouse (subclass 0, protocol 2), EP 0x81 IN 8 + cfg.i0 = (struct usb_interface_descriptor){ .bLength = USB_DT_INTERFACE_SIZE, .bDescriptorType = USB_DT_INTERFACE, + .bInterfaceNumber = 0, .bNumEndpoints = 1, .bInterfaceClass = 3, .bInterfaceSubClass = 0, .bInterfaceProtocol = 2 }; + cfg.h0 = (struct hid_desc){ sizeof(struct hid_desc), HID_DT, 0x0110, 0, 1, HID_RPT_DT, sizeof(RDESC_MOUSE) }; + cfg.e0 = (struct ep_desc7){ .bLength = USB_DT_ENDPOINT_SIZE, .bDescriptorType = USB_DT_ENDPOINT, + .bEndpointAddress = 0x81, .bmAttributes = USB_ENDPOINT_XFER_INT, .wMaxPacketSize = 8, .bInterval = 4 }; + // iface 1: keyboard (subclass 1 boot, protocol 1), EP 0x82 IN 8 + cfg.i1 = (struct usb_interface_descriptor){ .bLength = USB_DT_INTERFACE_SIZE, .bDescriptorType = USB_DT_INTERFACE, + .bInterfaceNumber = 1, .bNumEndpoints = 1, .bInterfaceClass = 3, .bInterfaceSubClass = 1, .bInterfaceProtocol = 1 }; + cfg.h1 = (struct hid_desc){ sizeof(struct hid_desc), HID_DT, 0x0110, 0, 1, HID_RPT_DT, sizeof(RDESC_KBD) }; + cfg.e1 = (struct ep_desc7){ .bLength = USB_DT_ENDPOINT_SIZE, .bDescriptorType = USB_DT_ENDPOINT, + .bEndpointAddress = 0x82, .bmAttributes = USB_ENDPOINT_XFER_INT, .wMaxPacketSize = 8, .bInterval = 4 }; + // iface 2: the controller (subclass 0, protocol 0), EP 0x83 IN 64 + cfg.i2 = (struct usb_interface_descriptor){ .bLength = USB_DT_INTERFACE_SIZE, .bDescriptorType = USB_DT_INTERFACE, + .bInterfaceNumber = 2, .bNumEndpoints = 1, .bInterfaceClass = 3, .bInterfaceSubClass = 0, .bInterfaceProtocol = 0 }; + cfg.h2 = (struct hid_desc){ sizeof(struct hid_desc), HID_DT, 0x0110, 33, 1, HID_RPT_DT, sizeof(RDESC_CTRL) }; + cfg.e2 = (struct ep_desc7){ .bLength = USB_DT_ENDPOINT_SIZE, .bDescriptorType = USB_DT_ENDPOINT, + .bEndpointAddress = 0x83, .bmAttributes = USB_ENDPOINT_XFER_INT, .wMaxPacketSize = 64, .bInterval = 4 }; + // Full 9-byte endpoint descriptors for EP_ENABLE (the ioctl wants struct usb_endpoint_descriptor). + #define MKFULL(F,E) do{ memset(&F,0,sizeof F); F.bLength=USB_DT_ENDPOINT_SIZE; F.bDescriptorType=USB_DT_ENDPOINT; \ + F.bEndpointAddress=E.bEndpointAddress; F.bmAttributes=E.bmAttributes; F.wMaxPacketSize=E.wMaxPacketSize; F.bInterval=E.bInterval; }while(0) + MKFULL(epfull0, cfg.e0); MKFULL(epfull1, cfg.e1); MKFULL(epfull2, cfg.e2); +} + +static int fd = -1; +static int ctrl_ep = -1; // raw handle for the controller IN endpoint +static volatile int running = 1; +static volatile int configured = 0; +static int do_stream = 1; // argv: "nostream" disables the input streamer +static int dbg = 1; +static __u8 last_feature_cmd = 0; // last SET_REPORT command on iface 2 + +static void log_line(const char *s){ fprintf(stderr, "%s\n", s); } + +static int ep0_write(const void *data, int len){ + char buf[sizeof(struct usb_raw_ep_io)+4096]; struct usb_raw_ep_io *io=(void*)buf; + io->ep=0; io->flags=0; io->length=len; if(len) memcpy(io->data,data,len); + int r=ioctl(fd, USB_RAW_IOCTL_EP0_WRITE, io); + if(r<0){ char m[80]; snprintf(m,sizeof m," !! ep0_write(len=%d) errno=%d", len, errno); log_line(m); } + return r; +} +static int ep0_read(void *data, int len){ + char buf[sizeof(struct usb_raw_ep_io)+4096]; struct usb_raw_ep_io *io=(void*)buf; + io->ep=0; io->flags=0; io->length=len; + int r=ioctl(fd, USB_RAW_IOCTL_EP0_READ, io); if(r>=0 && data) memcpy(data, io->data, rwIndex & 0xff; + if(dbg){ char m[128]; snprintf(m,sizeof m," CTRL bRT=0x%02x bR=0x%02x wV=0x%04x wI=0x%04x wL=%u", + ctrl->bRequestType, ctrl->bRequest, ctrl->wValue, ctrl->wIndex, ctrl->wLength); log_line(m); } + // Standard requests + if((ctrl->bRequestType & USB_TYPE_MASK) == USB_TYPE_STANDARD){ + switch(ctrl->bRequest){ + case USB_REQ_GET_DESCRIPTOR: { + int type = ctrl->wValue >> 8, di = ctrl->wValue & 0xff; + if(type==USB_DT_DEVICE){ ep0_write(&dev_desc, dev_desc.bLength); return; } + if(type==USB_DT_CONFIG){ int l=sizeof(cfg); if(l>ctrl->wLength) l=ctrl->wLength; ep0_write(&cfg, l); return; } + if(type==USB_DT_STRING){ __u8 s[260]; int l=build_string(di,s); if(l>ctrl->wLength) l=ctrl->wLength; ep0_write(s,l); return; } + if(type==HID_RPT_DT){ // HID report descriptor for the interface in wIndex + const __u8 *r; int l; + if(idx==0){ r=RDESC_MOUSE; l=sizeof(RDESC_MOUSE);} else if(idx==1){ r=RDESC_KBD; l=sizeof(RDESC_KBD);} else { r=RDESC_CTRL; l=sizeof(RDESC_CTRL);} + if(l>ctrl->wLength) l=ctrl->wLength; ep0_write(r,l); return; + } + if(type==HID_DT){ struct hid_desc *h = idx==0?&cfg.h0:idx==1?&cfg.h1:&cfg.h2; ep0_write(h,h->bLength); return; } + ep0_stall(); return; + } + case USB_REQ_SET_CONFIGURATION: { + __u32 power = 0x32; ioctl(fd, USB_RAW_IOCTL_VBUS_DRAW, power); + ioctl(fd, USB_RAW_IOCTL_CONFIGURE); + enable_endpoints(); + ep0_ack(); // OUT/no-data: complete via a zero-length read + configured = 1; log_line(" SET_CONFIG: done"); + return; + } + case USB_REQ_SET_INTERFACE: ep0_ack(); return; + case USB_REQ_GET_STATUS: { __u16 s=0; ep0_write(&s,2); return; } + default: ep0_stall(); return; + } + } + // HID class requests + if((ctrl->bRequestType & USB_TYPE_MASK) == USB_TYPE_CLASS){ + switch(ctrl->bRequest){ + case 0x01: { // GET_REPORT + // Reply the serial-style feature blob for the controller (iface 2); harmless for others. + __u8 rep[64]; memset(rep,0,sizeof rep); + // Reply [cmd, len, 0x01, serial...] echoing the last requested command (serial = 0xAE). + const char *serial = "PFDECK0001"; + rep[0]=last_feature_cmd?last_feature_cmd:0xAE; rep[1]=strlen(serial); rep[2]=0x01; + memcpy(rep+3, serial, strlen(serial)); + int l=ctrl->wLength>64?64:ctrl->wLength; ep0_write(rep,l); return; + } + case 0x09: { // SET_REPORT — read the host's data, remember the command byte + __u8 buf[64]; int r=ep0_read(buf,ctrl->wLength>64?64:ctrl->wLength); + if(r>0) last_feature_cmd = buf[0]; // unnumbered report: data[0] is the command + return; // ep0_read consumes the data stage + acks + } + case 0x0a: ep0_ack(); return; // SET_IDLE (OUT/no-data) + case 0x0b: ep0_ack(); return; // SET_PROTOCOL (OUT/no-data) + case 0x03: { __u8 z=0; ep0_write(&z,1); return; } // GET_PROTOCOL + default: ep0_stall(); return; + } + } + ep0_stall(); +} + +static void *stream_thread(void *arg){ + (void)arg; __u8 rep[64]; __u32 seq=0; + while(running){ + if(configured && ctrl_ep>=0){ + memset(rep,0,sizeof rep); + rep[0]=0x01; rep[1]=0x00; rep[2]=0x09; rep[3]=0x3c; memcpy(rep+4,&seq,4); seq++; + char buf[sizeof(struct usb_raw_ep_io)+64]; struct usb_raw_ep_io *io=(void*)buf; + io->ep=ctrl_ep; io->flags=0; io->length=64; memcpy(io->data,rep,64); + ioctl(fd, USB_RAW_IOCTL_EP_WRITE, io); // blocks until the host polls the int IN ep + } + struct timespec ts={0, 8*1000*1000}; nanosleep(&ts,NULL); + } + return NULL; +} + +int main(int argc, char **argv){ + int seconds = argc>1?atoi(argv[1]):120; + for(int i=1;i=seconds) break; + ev->type=0; ev->length=sizeof(struct usb_ctrlrequest); + if(ioctl(fd, USB_RAW_IOCTL_EVENT_FETCH, ev)<0){ if(running) perror("EVENT_FETCH"); break; } + if(ev->type==USB_RAW_EVENT_CONNECT){ log_line("CONNECT"); } + else if(ev->type==USB_RAW_EVENT_CONTROL){ handle_control((struct usb_ctrlrequest*)ev->data); } + } + running=0; if(do_stream) pthread_join(th,NULL); + log_line("exiting"); + return 0; +}