| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320 |
- // usb_descriptors.c — mode-aware TinyUSB descriptors.
- //
- // Puck modes (Steam/Lizard) present VID/PID 28DE:1304 with four HID slot
- // interfaces (the cloned puck descriptor) + a WebUSB vendor interface. Emulated
- // modes (Xbox/Switch/PS5/PS3/…) present that mode's controller as ONE HID
- // gamepad + the same WebUSB vendor interface (so the panel works in every mode).
- // The active mode is read from flash at boot; a mode switch persists + reboots.
- //
- // SPDX-License-Identifier: AGPL-3.0-or-later
- #include <string.h>
- #include "tusb.h"
- #include "usb/usb_descriptors.h"
- #include "usb/puck_desc.h"
- #include "puck/identity.h"
- #include "puck/emu.h"
- #include "config/modes.h"
- #include "sys/settings.h"
- void webusb_set_connected(bool connected);
- #if CFG_TUD_CDC
- #define PP_BCD PP_BCD_DEVICE_CDC
- #else
- #define PP_BCD PP_BCD_DEVICE
- #endif
- // ---- cached per-boot presentation ------------------------------------------
- static uint8_t s_mode;
- static const emu_mode_t *s_emu; // NULL for puck/xinput modes
- static bool s_xinput; // MODE_XBOX: custom vendor class, not HID
- static uint8_t s_emu_ifaces = 1; // # of emulated HID interfaces (one per controller)
- static tusb_desc_device_t s_dev;
- static uint8_t s_emu_cfg[220]; // PP_NSLOT HID (32 B each) + config + vendor
- static uint8_t s_vendor_itf;
- uint8_t usb_emu_iface_count(void) { return s_emu_ifaces; }
- #define MS_OS_20_DESC_LEN 0xB2
- static uint8_t s_msos[MS_OS_20_DESC_LEN];
- // Build the emulated-mode config descriptor: `nif` HID gamepad interfaces (one
- // per controller slot, each with an IN + OUT endpoint) followed by the WebUSB
- // vendor interface. Presenting several HID interfaces is what lets more than one
- // connected controller appear to the host at once (each interface = one pad).
- // Returns total length. HID endpoints: IN 0x81+i / OUT 0x01+i; vendor 0x85/0x05.
- static uint16_t build_emu_config(uint8_t *b, const emu_mode_t *emu, uint8_t nif)
- {
- uint16_t rlen = emu->report_desc_len;
- uint8_t poll = emu->poll_ms ? emu->poll_ms : 1;
- if (nif < 1) nif = 1;
- if (nif > PP_NSLOT) nif = PP_NSLOT;
- const uint16_t total = 9 + nif * (9 + 9 + 7 + 7) + (9 + 7 + 7);
- uint8_t *p = b;
- // configuration
- *p++ = 9; *p++ = TUSB_DESC_CONFIGURATION;
- *p++ = (uint8_t)(total & 0xFF); *p++ = (uint8_t)(total >> 8);
- *p++ = (uint8_t)(nif + 1); *p++ = 1; *p++ = 0; *p++ = 0x80; *p++ = 250;
- for (uint8_t i = 0; i < nif; i++) {
- // HID interface i — IN + OUT (rumble / Switch-Pro subcommands ride the
- // interrupt OUT pipe; output reports also work via EP0 SET_REPORT).
- *p++ = 9; *p++ = TUSB_DESC_INTERFACE; *p++ = i; *p++ = 0; *p++ = 2;
- *p++ = TUSB_CLASS_HID; *p++ = 0; *p++ = 0; *p++ = 0;
- *p++ = 9; *p++ = HID_DESC_TYPE_HID; *p++ = 0x11; *p++ = 0x01; *p++ = 0;
- *p++ = 1; *p++ = HID_DESC_TYPE_REPORT;
- *p++ = (uint8_t)(rlen & 0xFF); *p++ = (uint8_t)(rlen >> 8);
- *p++ = 7; *p++ = TUSB_DESC_ENDPOINT; *p++ = (uint8_t)(0x81 + i); *p++ = 0x03;
- *p++ = 64; *p++ = 0; *p++ = poll;
- *p++ = 7; *p++ = TUSB_DESC_ENDPOINT; *p++ = (uint8_t)(0x01 + i); *p++ = 0x03;
- *p++ = 64; *p++ = 0; *p++ = poll;
- }
- // vendor interface (last) — WebUSB
- *p++ = 9; *p++ = TUSB_DESC_INTERFACE; *p++ = nif; *p++ = 0; *p++ = 2;
- *p++ = TUSB_CLASS_VENDOR_SPECIFIC; *p++ = 0; *p++ = 0; *p++ = 0;
- *p++ = 7; *p++ = TUSB_DESC_ENDPOINT; *p++ = 0x05; *p++ = 0x02;
- *p++ = 64; *p++ = 0; *p++ = 0;
- *p++ = 7; *p++ = TUSB_DESC_ENDPOINT; *p++ = 0x85; *p++ = 0x02;
- *p++ = 64; *p++ = 0; *p++ = 0;
- return total;
- }
- // Build the XInput config: the WebUSB vendor interface FIRST (itf 0), so the
- // built-in vendor class driver claims it and — with only one vendor instance —
- // leaves the XInput interface (itf 1, class 0xFF/0x5D/0x01) for the custom
- // XInput app driver. Endpoints: vendor bulk 0x02/0x82; XInput interrupt IN
- // 0x83 / OUT 0x03. Returns total length.
- static uint16_t build_xinput_config(uint8_t *b)
- {
- const uint16_t total = 9 + (9 + 7 + 7) + (9 + 17 + 7 + 7); // 72
- uint8_t *p = b;
- // configuration (2 interfaces)
- *p++ = 9; *p++ = TUSB_DESC_CONFIGURATION;
- *p++ = (uint8_t)(total & 0xFF); *p++ = (uint8_t)(total >> 8);
- *p++ = 2; *p++ = 1; *p++ = 0; *p++ = 0x80; *p++ = 250;
- // vendor interface (itf 0) — WebUSB
- *p++ = 9; *p++ = TUSB_DESC_INTERFACE; *p++ = 0; *p++ = 0; *p++ = 2;
- *p++ = TUSB_CLASS_VENDOR_SPECIFIC; *p++ = 0; *p++ = 0; *p++ = 4;
- *p++ = 7; *p++ = TUSB_DESC_ENDPOINT; *p++ = 0x02; *p++ = 0x02;
- *p++ = 64; *p++ = 0; *p++ = 0;
- *p++ = 7; *p++ = TUSB_DESC_ENDPOINT; *p++ = 0x82; *p++ = 0x02;
- *p++ = 64; *p++ = 0; *p++ = 0;
- // XInput interface (itf 1) — vendor class 0xFF/0x5D/0x01, 2 interrupt eps
- *p++ = 9; *p++ = TUSB_DESC_INTERFACE; *p++ = 1; *p++ = 0; *p++ = 2;
- *p++ = 0xFF; *p++ = 0x5D; *p++ = 0x01; *p++ = 0;
- // XInput "unknown" 0x21 descriptor (verbatim; bytes 6/13 = IN/OUT ep addr)
- *p++ = 0x11; *p++ = 0x21; *p++ = 0x00; *p++ = 0x01; *p++ = 0x01; *p++ = 0x25;
- *p++ = 0x83; *p++ = 0x14; *p++ = 0x00; *p++ = 0x00; *p++ = 0x00; *p++ = 0x00;
- *p++ = 0x13; *p++ = 0x03; *p++ = 0x08; *p++ = 0x00; *p++ = 0x00;
- // XInput IN endpoint (interrupt, 0x83, 1 ms)
- *p++ = 7; *p++ = TUSB_DESC_ENDPOINT; *p++ = 0x83; *p++ = 0x03;
- *p++ = 0x20; *p++ = 0; *p++ = 1;
- // XInput OUT endpoint (interrupt, 0x03, 8 ms)
- *p++ = 7; *p++ = TUSB_DESC_ENDPOINT; *p++ = 0x03; *p++ = 0x03;
- *p++ = 0x20; *p++ = 0; *p++ = 8;
- return total;
- }
- // MS OS 2.0 template (WINUSB compatible id + DeviceInterfaceGUIDs). The
- // function-subset "first interface" byte is patched per mode at init.
- static const uint8_t k_msos_template[MS_OS_20_DESC_LEN] = {
- U16_TO_U8S_LE(0x000A), U16_TO_U8S_LE(MS_OS_20_SET_HEADER_DESCRIPTOR),
- U32_TO_U8S_LE(0x06030000), U16_TO_U8S_LE(MS_OS_20_DESC_LEN),
- U16_TO_U8S_LE(0x0008), U16_TO_U8S_LE(MS_OS_20_SUBSET_HEADER_CONFIGURATION),
- 0, 0, U16_TO_U8S_LE(MS_OS_20_DESC_LEN - 0x0A),
- U16_TO_U8S_LE(0x0008), U16_TO_U8S_LE(MS_OS_20_SUBSET_HEADER_FUNCTION),
- 0 /* first interface, patched */, 0,
- U16_TO_U8S_LE(MS_OS_20_DESC_LEN - 0x0A - 0x08),
- U16_TO_U8S_LE(0x0014), U16_TO_U8S_LE(MS_OS_20_FEATURE_COMPATBLE_ID),
- 'W', 'I', 'N', 'U', 'S', 'B', 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
- U16_TO_U8S_LE(MS_OS_20_DESC_LEN - 0x0A - 0x08 - 0x08 - 0x14),
- U16_TO_U8S_LE(MS_OS_20_FEATURE_REG_PROPERTY),
- U16_TO_U8S_LE(0x0007), U16_TO_U8S_LE(0x002A),
- 'D', 0x00, 'e', 0x00, 'v', 0x00, 'i', 0x00, 'c', 0x00, 'e', 0x00, 'I', 0x00,
- 'n', 0x00, 't', 0x00, 'e', 0x00, 'r', 0x00, 'f', 0x00, 'a', 0x00, 'c', 0x00,
- 'e', 0x00, 'G', 0x00, 'U', 0x00, 'I', 0x00, 'D', 0x00, 's', 0x00, 0x00, 0x00,
- U16_TO_U8S_LE(0x0050),
- '{', 0x00, '9', 0x00, '7', 0x00, '5', 0x00, 'F', 0x00, '4', 0x00, '4', 0x00,
- 'D', 0x00, '9', 0x00, '-', 0x00, '0', 0x00, 'D', 0x00, '0', 0x00, '8', 0x00,
- '-', 0x00, '4', 0x00, '3', 0x00, 'F', 0x00, 'D', 0x00, '-', 0x00, '8', 0x00,
- 'B', 0x00, '3', 0x00, 'E', 0x00, '-', 0x00, '1', 0x00, '2', 0x00, '7', 0x00,
- 'C', 0x00, 'A', 0x00, '8', 0x00, 'A', 0x00, 'F', 0x00, 'F', 0x00, 'F', 0x00,
- '9', 0x00, 'D', 0x00, '}', 0x00, 0x00, 0x00, 0x00, 0x00,
- };
- void usb_descriptors_init(void)
- {
- s_mode = settings_mode();
- s_xinput = mode_is_xinput(s_mode);
- s_emu = (mode_is_puck(s_mode) || s_xinput) ? NULL : emu_mode_for(s_mode);
- if (!mode_is_puck(s_mode) && !s_xinput && !s_emu)
- s_mode = MODE_STEAM; // unknown/unimplemented emu mode → puck fallback
- memset(&s_dev, 0, sizeof(s_dev));
- s_dev.bLength = sizeof(tusb_desc_device_t);
- s_dev.bDescriptorType = TUSB_DESC_DEVICE;
- s_dev.bcdUSB = 0x0210;
- s_dev.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE;
- s_dev.iManufacturer = 0x01;
- s_dev.iProduct = 0x02;
- s_dev.iSerialNumber = 0x03;
- s_dev.bNumConfigurations = 0x01;
- if (s_xinput) {
- // Xbox 360 wired: Windows xusb / Linux xpad / SDL bind 045E:028E.
- s_dev.idVendor = 0x045E;
- s_dev.idProduct = 0x028E;
- s_dev.bcdDevice = 0x0120;
- build_xinput_config(s_emu_cfg);
- s_vendor_itf = 0; // WebUSB vendor is itf 0 in the XInput config
- } else if (s_emu) {
- s_dev.idVendor = s_emu->vid;
- s_dev.idProduct = s_emu->pid;
- s_dev.bcdDevice = s_emu->bcd;
- // One HID interface per paired controller (min 1) so several connected
- // controllers each appear as their own pad. Bond count is read from the
- // persisted label table (BT isn't up yet at descriptor-build time). New
- // pairings made while already in an emulated mode need a replug/reboot to
- // gain an interface.
- uint8_t nb = 0;
- const pp_cfg_t *c = settings();
- for (int i = 0; i < 4; i++)
- if (c->bond[i].used) nb++;
- s_emu_ifaces = nb < 1 ? 1 : (nb > PP_NSLOT ? PP_NSLOT : nb);
- build_emu_config(s_emu_cfg, s_emu, s_emu_ifaces);
- s_vendor_itf = s_emu_ifaces; // vendor is the interface after the HIDs
- } else {
- s_dev.idVendor = PP_USB_VID;
- s_dev.idProduct = PP_USB_PID;
- s_dev.bcdDevice = PP_BCD;
- s_vendor_itf = ITF_NUM_VENDOR;
- }
- memcpy(s_msos, k_msos_template, MS_OS_20_DESC_LEN);
- s_msos[22] = s_vendor_itf; // function-subset "first interface"
- }
- const uint8_t *tud_descriptor_device_cb(void)
- {
- return (const uint8_t *)&s_dev;
- }
- const uint8_t *tud_hid_descriptor_report_cb(uint8_t instance)
- {
- (void)instance;
- return s_emu ? s_emu->report_desc : PUCK_HID_DESC;
- }
- // ---- puck configuration descriptor -----------------------------------------
- #define PUCK_CONFIG_TOTAL_LEN \
- (TUD_CONFIG_DESC_LEN + 4 * TUD_HID_DESC_LEN + TUD_VENDOR_DESC_LEN + \
- CFG_TUD_CDC * TUD_CDC_DESC_LEN)
- static const uint8_t desc_configuration_puck[] = {
- TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, PUCK_CONFIG_TOTAL_LEN, 0x00, 250),
- TUD_HID_DESCRIPTOR(ITF_NUM_HID0, 0, HID_ITF_PROTOCOL_NONE,
- PUCK_HID_DESC_SIZE, EPNUM_HID0, CFG_TUD_HID_EP_BUFSIZE, 1),
- TUD_HID_DESCRIPTOR(ITF_NUM_HID1, 0, HID_ITF_PROTOCOL_NONE,
- PUCK_HID_DESC_SIZE, EPNUM_HID1, CFG_TUD_HID_EP_BUFSIZE, 1),
- TUD_HID_DESCRIPTOR(ITF_NUM_HID2, 0, HID_ITF_PROTOCOL_NONE,
- PUCK_HID_DESC_SIZE, EPNUM_HID2, CFG_TUD_HID_EP_BUFSIZE, 1),
- TUD_HID_DESCRIPTOR(ITF_NUM_HID3, 0, HID_ITF_PROTOCOL_NONE,
- PUCK_HID_DESC_SIZE, EPNUM_HID3, CFG_TUD_HID_EP_BUFSIZE, 1),
- TUD_VENDOR_DESCRIPTOR(ITF_NUM_VENDOR, 4, EPNUM_VENDOR_OUT, EPNUM_VENDOR_IN, 64),
- #if CFG_TUD_CDC
- TUD_CDC_DESCRIPTOR(ITF_NUM_CDC, 5, EPNUM_CDC_NOTIF, 8, EPNUM_CDC_OUT,
- EPNUM_CDC_IN, 64),
- #endif
- };
- const uint8_t *tud_descriptor_configuration_cb(uint8_t index)
- {
- (void)index;
- return (s_emu || s_xinput) ? s_emu_cfg : desc_configuration_puck;
- }
- // ---- BOS + MS OS 2.0 --------------------------------------------------------
- #define BOS_TOTAL_LEN \
- (TUD_BOS_DESC_LEN + TUD_BOS_WEBUSB_DESC_LEN + \
- TUD_BOS_MICROSOFT_OS_DESC_LEN)
- static const uint8_t desc_bos[] = {
- TUD_BOS_DESCRIPTOR(BOS_TOTAL_LEN, 2),
- TUD_BOS_WEBUSB_DESCRIPTOR(VENDOR_REQUEST_WEBUSB, 0),
- TUD_BOS_MS_OS_20_DESCRIPTOR(MS_OS_20_DESC_LEN, VENDOR_REQUEST_MICROSOFT),
- };
- const uint8_t *tud_descriptor_bos_cb(void)
- {
- return desc_bos;
- }
- bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage,
- tusb_control_request_t const *request)
- {
- if (stage != CONTROL_STAGE_SETUP)
- return true;
- switch (request->bmRequestType_bit.type) {
- case TUSB_REQ_TYPE_VENDOR:
- if (request->bRequest == VENDOR_REQUEST_MICROSOFT &&
- request->wIndex == 7) {
- uint16_t total_len;
- memcpy(&total_len, s_msos + 8, 2);
- return tud_control_xfer(rhport, request,
- (void *)(uintptr_t)s_msos, total_len);
- }
- return false;
- case TUSB_REQ_TYPE_CLASS:
- if (request->bRequest == 0x22) {
- webusb_set_connected(request->wValue != 0);
- return tud_control_status(rhport, request);
- }
- break;
- default:
- break;
- }
- return false;
- }
- // ---- string descriptors ----------------------------------------------------
- enum { STRID_LANGID = 0, STRID_MANUFACTURER, STRID_PRODUCT, STRID_SERIAL, STRID_VENDOR };
- static uint16_t desc_str[32];
- const uint16_t *tud_descriptor_string_cb(uint8_t index, uint16_t langid)
- {
- (void)langid;
- size_t chr_count;
- if (index == STRID_LANGID) {
- desc_str[1] = 0x0409;
- chr_count = 1;
- } else {
- const char *str;
- switch (index) {
- case STRID_MANUFACTURER:
- str = s_emu ? "PicoPuck" : "Valve Software";
- break;
- case STRID_PRODUCT:
- str = s_emu ? s_emu->product : "Steam Controller Puck";
- break;
- case STRID_SERIAL:
- str = g_usb_serial;
- break;
- case STRID_VENDOR:
- str = "PicoPuck WebUSB";
- break;
- default:
- return NULL;
- }
- if (!str)
- return NULL;
- chr_count = strlen(str);
- if (chr_count > 31)
- chr_count = 31;
- for (size_t i = 0; i < chr_count; i++)
- desc_str[1 + i] = str[i];
- }
- desc_str[0] = (uint16_t)((TUSB_DESC_STRING << 8) | (2 * chr_count + 2));
- return desc_str;
- }
|