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:
@@ -71,9 +71,27 @@ real module): it enumerates the 3-interface Deck, hid-steam binds it + reads our
|
||||
musl, `ioctl(fd, RUN)` with no third arg passes a garbage `value`, and raw_gadget's `RUN`/`CONFIGURE`/
|
||||
`EP0_STALL` reject a non-zero `value` with `EINVAL` — so the no-arg ioctls must pass an explicit `0`.
|
||||
|
||||
## Feature contract (hardened — churn fixed)
|
||||
|
||||
Steam's `GetControllerInfo` reads HID **feature reports**; serving the real ones is what stops Steam
|
||||
re-probing (which was destroying + recreating the gamepad evdev — the "churn"). The contract was
|
||||
captured from a physical Deck (`get_deck_attrs.c`, hidraw `HIDIOCGFEATURE`; usbmon truncates to 32B):
|
||||
|
||||
- **`0x83` GET_ATTRIBUTES_VALUES** — `[83, 2d, 9× (attr-id, u32-LE)]`: product id `0x1205`, a unit
|
||||
serial (`0x0a`/`0x04` — we stamp a per-instance value so a gadget never collides with a real Deck),
|
||||
and capability attrs (`0x09=0x2e`, `0x0b=0x0fa0`, `0x02/0x0c/0x0d/0x0e=0`). **This blob is the fix.**
|
||||
- **`0xAE` GET_STRING_ATTRIBUTE** — `[ae, len, attr, ascii]`: serial (attr 1), board serial (attr 0).
|
||||
- Other commands (e.g. `0x87` settings) read back the last write (echo).
|
||||
|
||||
Result on the Deck (`feature_reply` in `steam_gadget.rs`): **1 connect / 0 disconnect / 1 gamepad
|
||||
evdev** (was constant churn), and Steam *activates* the controller cleanly (no `GetControllerInfo
|
||||
failed`, no zombie) and emits its **X-Box 360 pad**. usbmon on the gadget's bus confirms our state
|
||||
reports (with the pressed button at byte 8) are delivered on the interrupt-IN and consumed by
|
||||
hid-steam — so the input transport is proven end-to-end.
|
||||
|
||||
## Remaining
|
||||
|
||||
- **Harden the feature contract** so Steam stops re-probing + the gamepad evdev stops churning (serve
|
||||
Steam's full `GetControllerInfo` attribute set, captured from a physical Deck) — then a clean live
|
||||
input-flow check + defaulting the gadget on for SteamOS hosts.
|
||||
- **Glass confirmation of the XInput mapping** — Steam Input only maps the gadget's raw input onto its
|
||||
X-Box pad while a game using Steam Input is focused; confirm a button reaches a real game, then
|
||||
default the gadget on for SteamOS hosts (it's strictly better than the non-promoted UHID path).
|
||||
- A `punktfunk-host` build for SteamOS to exercise the integrated path end-to-end with a live client.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user