feat(host/steam): harden the gadget feature contract — fixes the evdev churn

The virtual Deck's gamepad evdev was churning (destroyed + recreated) because
Steam kept re-probing: GetControllerInfo reads HID feature reports, and the gadget
served zeros for them. Captured the real contract off a physical Deck
(packaging/linux/steam-deck-gadget/get_deck_attrs.c, hidraw HIDIOCGFEATURE — usbmon
truncates to 32B) and implemented it in steam_gadget.rs::feature_reply:

- 0x83 GET_ATTRIBUTES_VALUES: [83, 2d, 9×(attr-id, u32-LE)] — product id 0x1205, a
  per-instance unit serial (0x0a/0x04, so a gadget never collides with a real Deck
  or another gadget), and the capability attrs (0x09=0x2e, 0x0b=0x0fa0, rest 0).
- 0xAE GET_STRING_ATTRIBUTE: [ae, len, attr, ascii] — serial (attr 1) / board
  serial (attr 0).
- other commands (0x87 settings): echo the last write.

Validated on the Deck: 1 connect / 0 disconnect / 1 gamepad evdev (was constant
churn), Steam activates the gadget cleanly (no GetControllerInfo failed, no zombie)
and emits its X-Box 360 pad. usbmon on the gadget's bus confirms our state reports
(pressed button at byte 8) are delivered on the interrupt-IN and consumed by
hid-steam — so with M1/M2's byte-8→BTN_SOUTH decode the input chain is proven
end-to-end. Remaining: a foreground-game confirmation of Steam Input's XInput
mapping, then default the gadget on for SteamOS.

Workspace clippy/fmt/test green. Not pushed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-29 16:01:19 +00:00
parent c8e19396e4
commit 7ab8acaf55
4 changed files with 130 additions and 17 deletions
@@ -0,0 +1,39 @@
// Query a physical Steam Deck's feature reports (SET command then GET response) over hidraw to get
// the FULL blobs (usbmon truncates to 32 bytes). Steam feature reports are unnumbered 64-byte; the
// hidraw buffer prefixes a report-id byte (0).
#include <linux/hidraw.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
static void dump(const char *name, unsigned char *b, int n) {
printf("%s rc=%d:", name, n);
for (int i = 0; i < (n > 0 ? n : 0); i++) printf(" %02x", b[i]);
printf("\n");
}
int main(int argc, char **argv) {
if (argc < 2) { printf("usage: %s /dev/hidrawN\n", argv[0]); return 1; }
int fd = open(argv[1], O_RDWR);
if (fd < 0) { perror("open"); return 1; }
// SET [reportid=0, cmd, len, attr, ...] then GET the response.
unsigned char queries[][4] = {
{0x83, 0x00, 0x00, 0x00}, // GET_ATTRIBUTES_VALUES
{0xae, 0x16, 0x01, 0x00}, // GET_STRING_ATTRIBUTE, serial (attr 1)
{0xae, 0x16, 0x00, 0x00}, // GET_STRING_ATTRIBUTE, attr 0
{0xae, 0x16, 0x02, 0x00}, // attr 2 (board serial?)
};
for (int q = 0; q < 4; q++) {
unsigned char set[65] = {0};
set[0] = 0; // report id 0
memcpy(set + 1, queries[q], 4);
int sr = ioctl(fd, HIDIOCSFEATURE(65), set);
usleep(3000);
unsigned char get[65] = {0};
get[0] = 0;
int gr = ioctl(fd, HIDIOCGFEATURE(65), get);
printf("=== query cmd=%02x attr=%02x (SET rc=%d) ===\n", queries[q][0], queries[q][2], sr);
dump(" GET", get, gr);
}
return 0;
}