5 Commits
Author SHA1 Message Date
Sub 2e243fa093 pad2c 1.0.0: Ultimate 2C dongle in XInput mode (fixes #2), signed notifications (#3)
* pad2c 0.2.2: Ultimate 2C dongle in XInput mode (fixes #2)

With the controller in XInput mode the receiver enumerates as 2dc8:310a
with an XInput interface (ff/5d/01), a boot keyboard and a vendor HID.
0.2.1 read the first HID interrupt IN endpoint: the keyboard.

- usbdesc.c picks the interface from the configuration descriptor:
  XInput first, from any vendor, then on an 8BitDo the first HID that
  is not a boot keyboard or mouse.
- mapxinput.c maps the Xbox 360 input report to the DualSense layout.
- usbpad.c opens the XInput OUT endpoint and sets the player LED, as a
  PC does.
- tests/test_usb.c: the 310a descriptor from the issue, the 301c one,
  and the XInput map.
- tools/setup-macos.sh: build on macOS with Homebrew LLVM.

Tested on a PS5 Slim, 13.60: DInput (301c), switch to XInput (310a),
and back, all buttons, sticks and triggers.

* Notifications: "pad2c by Sub", then the controller, its mode and what happened

Every notification now reads

    pad2c by Sub
    8BitDo Ultimate 2C Wireless Controller (XInput) connected

The name is read from the device's product string descriptor with a
standard GET_DESCRIPTOR (USB_GET_DEVICEINFO fails on the 8BitDo dongle).
The receiver names itself the same in DInput (301c) and XInput (310a),
so the mode is shown beside it.

- usbdesc_string(): UTF-16LE string descriptor to ASCII, with tests.
- usbpad_mode(): "DInput" or "XInput".
- All messages in English.

Tested on the PS5: connect, disconnect, XInput and back.

* Version 1.0.0; CODEOWNERS: every change needs the owner's review

The XInput fix and the signed notifications are released as 1.0.0.
.github/CODEOWNERS names @sub0x00001 for every file, so the main
ruleset's code owner review asks for the owner on every pull request.
2026-10-22 15:38:30 -03:00
sub0x00001 6de27e3aea README: screenshots of the controller connecting and disconnecting 2026-10-06 09:55:10 -03:00
sub0x00001 7f2f0822cf README: the controller is a cheap second controller 2026-10-06 09:42:13 -03:00
sub0x00001 a41f640b7b README: why this controller and why the dongle 2026-10-06 09:41:23 -03:00
sub0x00001 25138ce0ab README: controller link, which controller is supported, FAQ; issue template for controller reports 2026-10-06 09:39:40 -03:00
17 changed files with 610 additions and 63 deletions

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
# Every change needs the review of the owner.
* @sub0x00001
@@ -0,0 +1,22 @@
---
name: Controller report
about: Tried pad2c with a controller (working or not), or want support for a new one
labels: controller
---
**Controller** (brand, model, connection: dongle / USB cable):
**Console** (model, e.g. CFI-2114; firmware; payload loader):
**What happened** (works / partly / not detected):
**pad2c log** (`/data/pad2c/pad2c.log`, the lines from the last start):
```
```
**For a controller that is not supported yet:** send `pad2c-probe.elf` with the controller plugged in, press every
button one at a time while it runs, and paste `/data/pad2c/probe.log` here.
```
```
+11
View File
@@ -1,5 +1,16 @@
# Changelog
## 1.0.0
- The Ultimate 2C receiver in XInput mode works (issue #2). When the controller connects, that receiver enumerates
as `2dc8:310a` with an XInput interface (`ff/5d/01`), a keyboard and a vendor HID; 0.2.1 read the keyboard. pad2c
now picks the interface from the descriptor: XInput first, from any vendor, then on an 8BitDo the first HID that is
not a boot keyboard or mouse. XInput reports are mapped like the DualSense layout, and the player LED is set as a
PC does. L4/R4 send nothing in XInput mode.
- Notifications: every one starts with "pad2c by Sub", then what happened, all in English, e.g.
"8BitDo Ultimate 2C Wireless Controller (XInput) connected". The name is the controller's own, read from its USB
string descriptor (`USB_GET_DEVICEINFO` fails on the dongle), and the mode is DInput or XInput.
- `tools/setup-macos.sh`: builds on macOS with Homebrew LLVM.
## 0.2.1
- "Turn off" in the console's menu works: the console calls `sceMbusDisconnectDevice` on the pad, which does nothing
to a virtual one (it stayed on, and the menu then showed it as a generic "Accessories" device). pad2c now removes
+3 -1
View File
@@ -11,6 +11,8 @@ test:
@mkdir -p build
cc -std=c11 -Wall -Wextra -Werror -Isrc -o build/test_map src/map8bitdo.c tests/test_map.c
./build/test_map
cc -std=c11 -Wall -Wextra -Werror -Isrc -o build/test_usb src/usbdesc.c src/mapxinput.c tests/test_usb.c
./build/test_usb
ifdef PS5_PAYLOAD_SDK
include $(PS5_PAYLOAD_SDK)/toolchain/prospero.mk
@@ -19,7 +21,7 @@ endif
CFLAGS := -std=gnu11 -Wall -Wextra -O2 -Isrc
LDLIBS := -lScePad -lSceUserService -ldl
SRCS := src/main.c src/usbpad.c src/map8bitdo.c src/ps5_vpad.c src/ps5_power.c src/lock.c src/log.c src/util.c
SRCS := src/main.c src/usbpad.c src/usbdesc.c src/map8bitdo.c src/mapxinput.c src/ps5_vpad.c src/ps5_power.c src/lock.c src/log.c src/util.c
dist/pad2c-$(VERSION).elf: $(SRCS) $(wildcard src/*.h)
@mkdir -p dist
+66 -8
View File
@@ -1,22 +1,77 @@
# pad2c
**Use an 8BitDo Ultimate 2C (2.4G dongle) as a second controller on a jailbroken PS5, next to your DualSense.**
**Use an [8BitDo Ultimate 2C Wireless Controller](https://www.8bitdo.com/ultimate-2c-wireless-controller/) on a
jailbroken PS5, as a second controller next to your DualSense.**
pad2c is a payload for a jailbroken PS5. It reads the 8BitDo 2.4G dongle plugged into the console's USB port and
shows it to the system and to games as a **virtual DualSense**. No Bluetooth is involved, so it never touches your
DualSense's pairing.
## Screenshots
Switching the 8BitDo on and off, with pad2c running on the console:
<p align="center">
<img src="docs/images/connected.jpg" width="45%" alt="The PS5 shows 'pad2c: 8BitDo connected' and asks who is using the controller">
&nbsp;
<img src="docs/images/disconnected.jpg" width="45%" alt="The PS5 shows 'pad2c: 8BitDo disconnected'">
</p>
<p align="center"><em>Left: switched on; the console asks who is using the controller. Right: switched off.</em></p>
## Which controller
| Controller | Supported |
|---|---|
| [8BitDo Ultimate 2C **Wireless** Controller](https://www.8bitdo.com/ultimate-2c-wireless-controller/) (the one sold for PC / Android, with a 2.4G USB dongle), through its dongle | ✅ |
| 8BitDo Ultimate 2C **Bluetooth** Controller (the Switch edition, no dongle) | ❌ different product |
| The Ultimate 2C Wireless over Bluetooth, without the dongle | ❌ not yet |
| Other 8BitDo 2.4G dongles | untested: run `pad2c-probe.elf` and open an issue with its output |
## Why this controller, and why the dongle
**The controller** is simply the one I own: pad2c was made to play with it on my PS5, next to the DualSense. It is
also a cheap way to get a second controller: a DualSense costs at least twice as much as the Ultimate 2C Wireless,
which still has Hall effect sticks and triggers.
**The dongle instead of Bluetooth**, on purpose:
- **No conflicts.** The PS5 has one Bluetooth chip, and the system uses it for the DualSense. Sharing it from a
payload risks taking over or unpairing the DualSense, and the chip is laid out differently from one console model
to another. The dongle is a plain USB device: pad2c never touches the console's Bluetooth.
- **Simpler.** No pairing, no Bluetooth stack to keep alive, no reconnection logic: the dongle and the controller
already talk to each other, and pad2c only reads what the dongle sends.
- **Practical.** Plug the dongle into a USB port and switch the controller on. That is all.
## FAQ
**Can I use an 8BitDo controller on a PS5?** Not on a stock PS5: it only accepts its own and a few licensed
controllers. On a jailbroken PS5, pad2c makes the 8BitDo Ultimate 2C appear as a DualSense.
**Does it work in games, or only in the menus?** In games too. Give the controller its own console user (the console
asks when you press Home) and it is player 2, like a second DualSense.
**Do I lose my DualSense?** No. Both work at the same time. pad2c does not use Bluetooth at all.
**Which firmware?** Tested on 13.60 (PS5 Slim). It needs a console that already runs a payload loader such as elfldr
or Payload Manager; it does not jailbreak anything itself.
## Status
| | |
|---|---|
| Tested on | PS5 Slim (CFI-2114), firmware **13.60**, elfldr 0.26, kstuff-lite 1.11, Payload Manager 0.5.2 |
| Controller | 8BitDo Ultimate 2C Wireless, 2.4G dongle (`2dc8:301c`) |
| Controller | 8BitDo Ultimate 2C Wireless, 2.4G dongle, DInput (`2dc8:301c`) and XInput (`2dc8:310a`) modes |
| Works | all buttons, sticks, analog triggers, d-pad, Home = PS, console menus and games, sleep and wake, the console's "who is using this controller" screen, remembers its user, "turn off" from the console's menu |
| Not yet | vibration, light bar, motion sensors, audio |
Other firmwares and console models are untested.
The controller has two modes on its dongle, both work. It keeps the one last chosen:
- **DInput** (the default): with the controller on, hold **B + Home** until it switches off, then **B + Home** again
until it switches on.
- **XInput** (seen by a PC as an Xbox controller): the same with **X + Home**. L4/R4 send nothing in this mode.
Notes:
- The 8BitDo has no touchpad: its extra **L4 / R4** buttons press the touchpad.
@@ -59,9 +114,11 @@ Files, all in `/data/pad2c/`:
- `src/usbpad.c` opens the dongle's `/dev/ugenB.A` node and reads its interrupt IN endpoint. The console's own HID
driver holds the interface but ignores the device. The internal Bluetooth chip (`ugen0.2`) is never touched.
- With no controller the dongle shows up as "8BitDo IDLE". When the controller connects, the dongle enumerates again
and sends report 1. `src/map8bitdo.c` turns that report into DualSense buttons (`make test` checks it against
reports captured on a console). When the controller sleeps, the dongle enumerates again and the open device ends.
- With no controller the dongle shows up as "8BitDo IDLE". When the controller connects, the dongle enumerates again:
in DInput mode it sends HID report 1, turned into DualSense buttons by `src/map8bitdo.c`; in XInput mode it becomes
`2dc8:310a` with an Xbox 360 interface, read by `src/mapxinput.c`. `src/usbdesc.c` picks the interface from the
USB descriptor (XInput first, then the 8BitDo HID, never a keyboard). `make test` checks both against reports and
descriptors captured on consoles. When the controller sleeps, the dongle enumerates again and the open device ends.
- `src/ps5_vpad.c` creates the virtual DualSense (`scePadVirtualDevice*`), finds its device id in the kernel log,
gives it to a user (`sceMbusBindDeviceWithUserId`) and feeds it. It follows the kernel log while running: the
console's choice of user is remembered, and its "turn off" (`sceMbusDisconnectDevice`) removes the virtual pad.
@@ -70,11 +127,12 @@ Files, all in `/data/pad2c/`:
## Build
Needs [ps5-payload-sdk](https://github.com/ps5-payload-dev/sdk) and clang, lld and llvm (Linux or WSL):
Needs [ps5-payload-sdk](https://github.com/ps5-payload-dev/sdk) and clang, lld and llvm. `tools/setup-wsl.sh`
(Linux or WSL) or `tools/setup-macos.sh` (Homebrew) installs them and writes `~/.ps5sdk`:
```
export PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk
make test # the button map, on the PC
. ~/.ps5sdk
make test # the button maps and the interface choice, on the PC
make # dist/pad2c-<version>.elf and dist/pad2c-probe.elf
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

+10 -8
View File
@@ -58,13 +58,14 @@ int main(void)
int vpad_made = 0; /* a virtual pad was asked for and not removed since */
int turned_off = 0; /* the console turned it off: Home brings it back */
int ps_latch = 0; /* PS (Home) held since before: not a new press */
char model[80] = "Controller"; /* "<name> (<mode>)" of the last controller, for the notifications */
mkdir(STATE_DIR, 0777);
log_open(LOG_PATH);
log_line("pad2c %s", PAD2C_VERSION);
if (!lock_take(LOCK_PATH)) {
log_line("another instance is running");
notify("pad2c is already running");
notify("Already running");
log_close();
return 1;
}
@@ -77,12 +78,12 @@ int main(void)
if (!vpad_init()) {
log_line("virtual pads not available; stopping");
notify("pad2c %s: cannot create a virtual DualSense (see %s)", PAD2C_VERSION, LOG_PATH);
notify("Cannot create a virtual DualSense\nSee %s", LOG_PATH);
lock_release();
log_close();
return 1;
}
notify("pad2c %s running\nSwitch the 8BitDo on (2.4G mode) with its dongle plugged in", PAD2C_VERSION);
notify("Version %s running\nSwitch the controller on with its dongle plugged in", PAD2C_VERSION);
pad_state_reset(&st);
while (!g_stop) {
@@ -102,7 +103,7 @@ int main(void)
vpad_remove(SLOT);
vpad_made = 0;
log_line("controller left");
notify("pad2c: 8BitDo disconnected");
notify("%s disconnected", model);
}
turned_off = 0; /* switched off for real: it starts afresh */
pad_state_reset(&st);
@@ -119,9 +120,10 @@ int main(void)
}
} else if (!vpad_made) {
vpad_made = vpad_add(SLOT);
log_line("controller connected (%s), virtual pad %s", usbpad_name(pad),
snprintf(model, sizeof model, "%s (%s)", usbpad_name(pad), usbpad_mode(pad));
log_line("controller connected: %s, virtual pad %s", model,
vpad_made ? "requested" : "refused");
notify("pad2c: 8BitDo connected");
notify("%s connected", model);
}
if (ps_latch) st.buttons &= ~(uint32_t)PAD_PS;
if (vpad_made) vpad_update(SLOT, &st);
@@ -141,7 +143,7 @@ int main(void)
turned_off = 1;
ps_latch = (st.buttons & PAD_PS) != 0;
log_line("virtual pad turned off; waiting for PS (Home)");
notify("pad2c: 8BitDo turned off\nPress Home on the controller to turn it back on");
notify("%s turned off\nPress Home to turn it back on", model);
}
if (now - last_power >= 200) {
@@ -170,6 +172,6 @@ int main(void)
lock_release();
log_line("stopped");
log_close();
notify("pad2c stopped (%s)", why);
notify("Stopped (%s)", why);
return 0;
}
+40
View File
@@ -0,0 +1,40 @@
#include "usbpad.h"
#include <stddef.h>
#include <stdint.h>
/* XInput (Xbox 360 wired protocol), input report:
* [0] 00 (type) [1] 14 (length)
* [2..3] buttons: up down left right Start Back L3 R3, LB RB Guide - A B X Y
* [4] LT [5] RT (0..255)
* [6..13] LX LY RX RY, int16 little-endian, centred on 0, y grows upwards
* Placed by position, as on a DualSense: A cross, B circle, X square, Y triangle.
* The Ultimate 2C sends nothing for L4/R4 in this mode. */
int usbpad_parse_xinput(const unsigned char *r, int n, pad_state *st)
{
static const struct { uint16_t bit; uint32_t pad; } map[] = {
{ 1u << 0, PAD_UP }, { 1u << 1, PAD_DOWN }, { 1u << 2, PAD_LEFT },
{ 1u << 3, PAD_RIGHT }, { 1u << 4, PAD_OPTIONS }, { 1u << 5, PAD_CREATE },
{ 1u << 6, PAD_L3 }, { 1u << 7, PAD_R3 }, { 1u << 8, PAD_L1 },
{ 1u << 9, PAD_R1 }, { 1u << 10, PAD_PS }, { 1u << 12, PAD_CROSS },
{ 1u << 13, PAD_CIRCLE }, { 1u << 14, PAD_SQUARE }, { 1u << 15, PAD_TRIANGLE },
};
unsigned b;
size_t i;
if (n < 14 || r[0] != 0x00 || r[1] < 0x14) return 0;
pad_state_reset(st);
b = (unsigned)r[2] | (unsigned)r[3] << 8;
for (i = 0; i < sizeof map / sizeof map[0]; i++)
if (b & map[i].bit) st->buttons |= map[i].pad;
st->l2 = r[4];
st->r2 = r[5];
/* -32768..32767 to 0..255; y flipped, as the DualSense grows downwards */
st->lx = (uint8_t)(((int16_t)(r[6] | r[7] << 8) + 32768) >> 8);
st->ly = (uint8_t)((32767 - (int16_t)(r[8] | r[9] << 8)) >> 8);
st->rx = (uint8_t)(((int16_t)(r[10] | r[11] << 8) + 32768) >> 8);
st->ry = (uint8_t)((32767 - (int16_t)(r[12] | r[13] << 8)) >> 8);
if (st->l2 > 24) st->buttons |= PAD_L2;
if (st->r2 > 24) st->buttons |= PAD_R2;
return 1;
}
+5 -1
View File
@@ -2,6 +2,7 @@
#include "host.h"
#include "log.h"
#include "util.h"
#include "version.h"
#include <sys/socket.h>
#include <sys/types.h>
@@ -177,12 +178,15 @@ static size_t g_line_len;
void notify(const char *fmt, ...)
{
/* Every notification: "pad2c by <author>", then what happened. */
static const char head[] = "pad2c by " PAD2C_AUTHOR "\n";
notify_request req;
va_list ap;
memset(&req, 0, sizeof req);
memcpy(req.message, head, sizeof head);
va_start(ap, fmt);
vsnprintf(req.message, sizeof req.message, fmt, ap);
vsnprintf(req.message + sizeof head - 1, sizeof req.message - (sizeof head - 1), fmt, ap);
va_end(ap);
sceKernelSendNotificationRequest(0, &req, sizeof req, 0);
}
+92
View File
@@ -0,0 +1,92 @@
#include "usbdesc.h"
#include <string.h>
#define DT_INTERFACE 0x04
#define DT_ENDPOINT 0x05
typedef struct {
uint8_t num, cls, sub, proto;
uint8_t ep_in, ep_out;
uint16_t mps_in, mps_out;
} iface;
static int is_xinput(const iface *f)
{
return f->cls == 0xFF && f->sub == 0x5D && f->proto == 0x01;
}
/* HID, but not a boot keyboard (1/1) or mouse (1/2). */
static int is_pad_hid(const iface *f)
{
return f->cls == 0x03 && !(f->sub == 0x01 && (f->proto == 0x01 || f->proto == 0x02));
}
static int pick(const iface *f, int kind, usbdesc_pick *out)
{
out->kind = kind;
out->iface = f->num;
out->ep_in = f->ep_in;
out->mps_in = f->mps_in;
out->ep_out = f->ep_out;
out->mps_out = f->mps_out;
return kind;
}
int usbdesc_choose(const unsigned char *cfg, int len, uint16_t vid, usbdesc_pick *out)
{
iface ifs[16];
int n = 0, off = 0, i;
memset(out, 0, sizeof *out);
while (off + 2 <= len) {
const unsigned char *d = cfg + off;
int dl = d[0];
if (dl < 2 || off + dl > len) break;
if (d[1] == DT_INTERFACE && dl >= 9) {
if (n == (int)(sizeof ifs / sizeof ifs[0])) break;
memset(&ifs[n], 0, sizeof ifs[n]);
ifs[n].num = d[2];
ifs[n].cls = d[5];
ifs[n].sub = d[6];
ifs[n].proto = d[7];
n++;
} else if (d[1] == DT_ENDPOINT && dl >= 7 && n > 0 && (d[3] & 3) == 3) {
iface *f = &ifs[n - 1];
uint16_t mps = (uint16_t)(d[4] | d[5] << 8);
if ((d[2] & 0x80) && !f->ep_in) {
f->ep_in = d[2];
f->mps_in = mps;
} else if (!(d[2] & 0x80) && !f->ep_out) {
f->ep_out = d[2];
f->mps_out = mps;
}
}
off += dl;
}
for (i = 0; i < n; i++)
if (is_xinput(&ifs[i]) && ifs[i].ep_in) return pick(&ifs[i], USBPAD_XINPUT, out);
if (vid == VID_8BITDO)
for (i = 0; i < n; i++)
if (is_pad_hid(&ifs[i]) && ifs[i].ep_in) return pick(&ifs[i], USBPAD_8BITDO, out);
return USBPAD_NONE;
}
int usbdesc_string(const unsigned char *d, int len, char *out, size_t size)
{
size_t n = 0;
int i;
if (size == 0) return 0;
out[0] = 0;
if (len < 2 || d[1] != 0x03) return 0;
if (d[0] < len) len = d[0];
for (i = 2; i + 1 < len && n + 1 < size; i += 2) {
unsigned c = d[i] | (unsigned)d[i + 1] << 8;
out[n++] = (char)(c >= 0x20 && c < 0x7F ? c : '?');
}
while (n > 0 && out[n - 1] == ' ') n--;
out[n] = 0;
return n > 0;
}
+35
View File
@@ -0,0 +1,35 @@
/* Which interface of a USB device pad2c reads, from its configuration
* descriptor alone (no device needed, so it runs in the tests too). */
#ifndef PAD2C_USBDESC_H
#define PAD2C_USBDESC_H
#include <stddef.h>
#include <stdint.h>
#define VID_8BITDO 0x2DC8
enum {
USBPAD_NONE = 0,
USBPAD_8BITDO, /* 8BitDo DInput: HID report 1 (map8bitdo.c) */
USBPAD_XINPUT, /* Xbox 360 protocol, vendor interface ff/5d/01 (mapxinput.c) */
};
typedef struct {
int kind;
uint8_t iface;
uint8_t ep_in, ep_out; /* ep_out 0: none */
uint16_t mps_in, mps_out;
} usbdesc_pick;
/* Picks the interface to read:
* 1. an XInput interface (class ff, subclass 5d, protocol 01), any vendor;
* 2. on an 8BitDo, the first HID interface that is not a boot keyboard or
* mouse (the 8BitDo 310a receiver also has a keyboard interface).
* Each needs an interrupt IN endpoint. Returns the kind, USBPAD_NONE if none. */
int usbdesc_choose(const unsigned char *cfg, int len, uint16_t vid, usbdesc_pick *out);
/* A string descriptor (UTF-16LE) as ASCII, '?' for anything else, trailing
* spaces dropped, cut to fit `size`. 0 if it is not one or is blank. */
int usbdesc_string(const unsigned char *d, int len, char *out, size_t size);
#endif
+112 -38
View File
@@ -1,5 +1,6 @@
#include "usbpad.h"
#include "log.h"
#include "usbdesc.h"
#include "util.h"
#include <sys/ioctl.h>
@@ -15,8 +16,8 @@
#include <string.h>
#include <unistd.h>
#define VID_8BITDO 0x2DC8
#define N_RD 4 /* reads kept pending on the IN endpoint */
#define EP_OUT N_RD /* transfer index of the OUT endpoint, if any */
#define RD_MAX 64
#define T_ALIVE 1000 /* ms between checks that the device is still there */
@@ -24,13 +25,14 @@ struct usbpad {
int fd;
char path[24];
char name[64];
uint8_t ep_in;
uint16_t mps;
struct usb_fs_endpoint ep[N_RD];
void *bufp[N_RD][1];
uint32_t lenp[N_RD][1];
unsigned char buf[N_RD][RD_MAX];
unsigned char busy[N_RD];
int kind; /* USBPAD_* (usbdesc.h) */
uint8_t ep_in, ep_out;
uint16_t mps, mps_out;
struct usb_fs_endpoint ep[N_RD + 1];
void *bufp[N_RD + 1][1];
uint32_t lenp[N_RD + 1][1];
unsigned char buf[N_RD + 1][RD_MAX];
unsigned char busy[N_RD + 1];
long t_alive;
int logged_first;
};
@@ -40,34 +42,68 @@ static int dead_errno(int e)
return e == ENXIO || e == ENODEV || e == EIO || e == EBADF || e == ENOENT;
}
/* The first interrupt IN endpoint of a HID interface. */
static int find_in_endpoint(int fd, uint8_t *ep, uint16_t *mps)
/* The interface to read, from the full configuration descriptor. */
static int choose_interface(int fd, uint16_t vid, usbdesc_pick *p)
{
struct usb_gen_descriptor gd;
unsigned char cfg[1024];
int off = 0, len, hid = 0;
int len;
memset(&gd, 0, sizeof gd);
gd.ugd_data = cfg;
gd.ugd_maxlen = sizeof cfg;
gd.ugd_config_index = 0xFF;
if (ioctl(fd, USB_GET_FULL_DESC, &gd) != 0) return 0;
if (ioctl(fd, USB_GET_FULL_DESC, &gd) != 0) return USBPAD_NONE;
len = gd.ugd_actlen < (int)sizeof cfg ? gd.ugd_actlen : (int)sizeof cfg;
while (off + 2 <= len) {
int dl = cfg[off];
const unsigned char *d = cfg + off;
if (dl < 2 || off + dl > len) break;
if (d[1] == 0x04 && dl >= 9) hid = d[5] == 0x03;
else if (d[1] == 0x05 && dl >= 7 && hid && (d[2] & 0x80) && (d[3] & 3) == 3) {
*ep = d[2];
*mps = (uint16_t)(d[4] | d[5] << 8);
return 1;
}
off += dl;
return usbdesc_choose(cfg, len, vid, p);
}
/* Sends one interrupt OUT transfer and waits for it, a short while. */
static int send_out(usbpad *u, const unsigned char *data, int n)
{
struct usb_fs_start st;
struct usb_fs_complete done;
long t0;
if (!u->ep_out || n > u->mps_out || n > RD_MAX) return 0;
memcpy(u->buf[EP_OUT], data, (size_t)n);
u->lenp[EP_OUT][0] = (uint32_t)n;
u->ep[EP_OUT].nFrames = 1;
u->ep[EP_OUT].aFrames = 0;
u->ep[EP_OUT].status = 0;
memset(&st, 0, sizeof st);
st.ep_index = EP_OUT;
if (ioctl(u->fd, USB_FS_START, &st) != 0) return 0;
u->busy[EP_OUT] = 1;
for (t0 = now_ms(); now_ms() - t0 < 100; usleep(1000)) {
memset(&done, 0, sizeof done);
if (ioctl(u->fd, USB_FS_COMPLETE, &done) != 0) continue;
u->busy[done.ep_index] = 0;
if (done.ep_index == EP_OUT) return u->ep[EP_OUT].status == 0;
}
return 0;
}
/* String descriptor `index` (US English), as ASCII. A standard GET_DESCRIPTOR. */
static int read_string(int fd, int index, char *out, size_t size)
{
struct usb_ctl_request req;
unsigned char buf[255];
if (index == 0) return 0;
memset(&req, 0, sizeof req);
memset(buf, 0, sizeof buf);
req.ucr_data = buf;
req.ucr_flags = 0x0004; /* USB_SHORT_XFER_OK (usbdi.h, kernel-only header) */
req.ucr_request.bmRequestType = UT_READ_DEVICE;
req.ucr_request.bRequest = UR_GET_DESCRIPTOR;
USETW(req.ucr_request.wValue, 0x0300 | index);
USETW(req.ucr_request.wIndex, 0x0409);
USETW(req.ucr_request.wLength, sizeof buf);
if (ioctl(fd, USB_DO_REQUEST, &req) != 0) return 0;
return usbdesc_string(buf, req.ucr_actlen, out, size);
}
static int start_read(usbpad *u, int i)
{
struct usb_fs_start st;
@@ -85,9 +121,11 @@ static int start_read(usbpad *u, int i)
static usbpad *try_node(int bus, int addr)
{
static const char *kinds[] = { "", "8BitDo HID (DInput)", "XInput" };
struct usb_device_descriptor dd;
struct usb_device_info di;
struct usb_fs_init init;
usbdesc_pick pick;
usbpad *u;
int i;
@@ -97,27 +135,38 @@ static usbpad *try_node(int bus, int addr)
u->fd = open(u->path, O_RDWR);
if (u->fd < 0) goto fail;
if (ioctl(u->fd, USB_GET_DEVICE_DESC, &dd) != 0) goto fail;
if (UGETW(dd.idVendor) != VID_8BITDO) goto fail;
if (!find_in_endpoint(u->fd, &u->ep_in, &u->mps)) goto fail;
u->kind = choose_interface(u->fd, UGETW(dd.idVendor), &pick);
if (u->kind == USBPAD_NONE) goto fail;
u->ep_in = pick.ep_in;
u->mps = pick.mps_in;
if (u->mps == 0 || u->mps > RD_MAX) u->mps = RD_MAX;
if (u->kind == USBPAD_XINPUT && pick.ep_out) {
u->ep_out = pick.ep_out;
u->mps_out = pick.mps_out ? pick.mps_out : 32;
if (u->mps_out > RD_MAX) u->mps_out = RD_MAX;
}
/* The product name: USB_GET_DEVICEINFO fails on the 8BitDo dongle, the
* string descriptor itself does not. */
memset(&di, 0, sizeof di);
if (ioctl(u->fd, USB_GET_DEVICEINFO, &di) == 0 && di.udi_product[0])
snprintf(u->name, sizeof u->name, "%.60s", di.udi_product);
else
snprintf(u->name, sizeof u->name, "8BitDo %04x", UGETW(dd.idProduct));
if (!read_string(u->fd, dd.iProduct, u->name, sizeof u->name)) {
if (ioctl(u->fd, USB_GET_DEVICEINFO, &di) == 0 && di.udi_product[0])
snprintf(u->name, sizeof u->name, "%.60s", di.udi_product);
else
snprintf(u->name, sizeof u->name, "%s", UGETW(dd.idVendor) == VID_8BITDO ? "8BitDo" : "USB controller");
}
for (i = 0; i < N_RD; i++) {
for (i = 0; i <= N_RD; i++) {
u->bufp[i][0] = u->buf[i];
u->lenp[i][0] = u->mps;
u->lenp[i][0] = i == EP_OUT ? u->mps_out : u->mps;
u->ep[i].ppBuffer = u->bufp[i];
u->ep[i].pLength = u->lenp[i];
u->ep[i].nFrames = 1;
u->ep[i].flags = USB_FS_FLAG_SINGLE_SHORT_OK;
u->ep[i].flags = i == EP_OUT ? 0 : USB_FS_FLAG_SINGLE_SHORT_OK;
}
memset(&init, 0, sizeof init);
init.pEndpoints = u->ep;
init.ep_index_max = N_RD;
init.ep_index_max = N_RD + 1;
if (ioctl(u->fd, USB_FS_INIT, &init) != 0) {
log_line("usb: %s: init errno %d", u->path, errno);
goto fail;
@@ -135,9 +184,27 @@ static usbpad *try_node(int bus, int addr)
return NULL;
}
}
if (u->ep_out) {
struct usb_fs_open op;
memset(&op, 0, sizeof op);
op.ep_index = EP_OUT;
op.ep_no = u->ep_out;
op.max_bufsize = u->mps_out;
op.max_frames = 1;
if (ioctl(u->fd, USB_FS_OPEN, &op) != 0) {
log_line("usb: %s: open endpoint %#x errno %d (no output)", u->path, u->ep_out, errno);
u->ep_out = 0;
}
}
u->t_alive = now_ms();
log_line("usb: %s %04x:%04x \"%s\", reading endpoint %#x (%u bytes)", u->path, UGETW(dd.idVendor),
UGETW(dd.idProduct), u->name, u->ep_in, u->mps);
log_line("usb: %s %04x:%04x \"%s\", %s interface %u, reading endpoint %#x (%u bytes)", u->path,
UGETW(dd.idVendor), UGETW(dd.idProduct), u->name, kinds[u->kind], pick.iface, u->ep_in, u->mps);
if (u->kind == USBPAD_XINPUT) {
/* The LED of player 1, as a PC sets it: some XInput pads report only after it. */
static const unsigned char led[3] = { 0x01, 0x03, 0x06 };
log_line("usb: %s: XInput LED %s", u->path,
!u->ep_out ? "not sent (no OUT endpoint)" : send_out(u, led, 3) ? "set" : "not acknowledged");
}
return u;
fail:
@@ -164,7 +231,12 @@ usbpad *usbpad_open(void)
const char *usbpad_name(const usbpad *u)
{
return u && u->name[0] ? u->name : "USB pad";
return u && u->name[0] ? u->name : "USB controller";
}
const char *usbpad_mode(const usbpad *u)
{
return u && u->kind == USBPAD_XINPUT ? "XInput" : "DInput";
}
int usbpad_poll(usbpad *u, pad_state *st)
@@ -188,11 +260,13 @@ int usbpad_poll(usbpad *u, pad_state *st)
break;
}
i = done.ep_index;
if (i >= N_RD) continue;
if (i > N_RD) continue;
u->busy[i] = 0;
if (i == EP_OUT) continue;
if (u->ep[i].status) continue; /* cancelled or stalled: read again */
if (!u->ep[i].aFrames) continue;
if (usbpad_parse_8bitdo(u->buf[i], (int)u->lenp[i][0], st)) {
if (u->kind == USBPAD_XINPUT ? usbpad_parse_xinput(u->buf[i], (int)u->lenp[i][0], st)
: usbpad_parse_8bitdo(u->buf[i], (int)u->lenp[i][0], st)) {
if (!u->logged_first) {
u->logged_first = 1;
log_line("usb: first controller report");
+12 -6
View File
@@ -2,11 +2,13 @@
* console's own HID driver (which holds the interface but does nothing with
* a device it does not know).
*
* Today: the 8BitDo 2.4G dongle (2dc8:301c). With no controller it shows up
* as "8BitDo IDLE" and sends nothing useful; when the controller connects it
* enumerates again as "8BitDo Ultimate 2C Wireless Controller" and sends
* report 1 (see usbpad.c). Every re-enumeration ends the open device, so the
* caller closes it and looks again.
* Today: the 8BitDo Ultimate 2C 2.4G dongle (2dc8:301c). With no controller it
* shows up as "8BitDo IDLE" and sends nothing useful; when the controller
* connects it enumerates again, as "8BitDo Ultimate 2C Wireless Controller"
* sending HID report 1 (DInput, map8bitdo.c), or, with the controller in
* XInput mode, as 2dc8:310a with an Xbox 360 interface (mapxinput.c). Which
* interface is read is chosen in usbdesc.c. Every re-enumeration ends the
* open device, so the caller closes it and looks again.
*/
#ifndef PAD2C_USBPAD_H
#define PAD2C_USBPAD_H
@@ -23,9 +25,13 @@ usbpad *usbpad_open(void);
int usbpad_poll(usbpad *u, pad_state *st);
void usbpad_close(usbpad *u);
const char *usbpad_name(const usbpad *u);
const char *usbpad_name(const usbpad *u); /* the product, as the device names itself */
const char *usbpad_mode(const usbpad *u); /* "DInput" or "XInput" */
/* The 8BitDo report 1 to a DualSense state. Returns 0 if it is not one. */
int usbpad_parse_8bitdo(const unsigned char *r, int n, pad_state *st);
/* An XInput (Xbox 360) input report to a DualSense state. 0 if it is not one. */
int usbpad_parse_xinput(const unsigned char *r, int n, pad_state *st);
#endif
+2 -1
View File
@@ -1,6 +1,7 @@
#ifndef PAD2C_VERSION_H
#define PAD2C_VERSION_H
#define PAD2C_VERSION "0.2.1"
#define PAD2C_VERSION "1.0.0"
#define PAD2C_AUTHOR "Sub" /* every notification starts "pad2c by Sub" */
#endif
+156
View File
@@ -0,0 +1,156 @@
/* Interface choice and the XInput mapping. Runs on the PC.
*
* The 310a descriptor is the one in issue #2 (pad2c-probe on firmware 13.00):
* the Ultimate 2C receiver in XInput mode. pad2c 0.2.1 read its keyboard. */
#include "usbdesc.h"
#include "usbpad.h"
#include <stdio.h>
#include <string.h>
static int fails;
#define CHECK(cond, ...) do { if (!(cond)) { printf("FAIL " __VA_ARGS__); printf("\n"); fails++; } } while (0)
/* configuration, interface, HID, endpoint descriptors */
#define CFG(total, n) 9, 0x02, (total) & 0xFF, (total) >> 8, n, 1, 0, 0xA0, 0xFA
#define IFACE(i, n, c, s, p) 9, 0x04, i, 0, n, c, s, p, 0
#define HID 9, 0x21, 0x11, 0x01, 0, 1, 0x22, 0x40, 0
#define EP(a, mps, iv) 7, 0x05, a, 0x03, (mps) & 0xFF, (mps) >> 8, iv
static const unsigned char desc_310a[] = {
CFG(9 + 9 + 7 + 7 + 9 + 9 + 7 + 9 + 9 + 7 + 7, 3),
IFACE(0, 2, 0xFF, 0x5D, 0x01), EP(0x84, 32, 1), EP(0x05, 32, 1),
IFACE(1, 1, 0x03, 0x01, 0x01), HID, EP(0x82, 16, 8),
IFACE(2, 2, 0x03, 0x00, 0x00), HID, EP(0x83, 64, 1), EP(0x06, 64, 1),
};
/* The receiver as 0.2.1 reads it: one HID interface. */
static const unsigned char desc_hid[] = {
CFG(9 + 9 + 9 + 7 + 7, 1),
IFACE(0, 2, 0x03, 0x00, 0x00), HID, EP(0x84, 64, 1), EP(0x03, 64, 1),
};
/* A boot keyboard and nothing else. */
static const unsigned char desc_kbd[] = {
CFG(9 + 9 + 9 + 7, 1),
IFACE(0, 1, 0x03, 0x01, 0x01), HID, EP(0x81, 8, 8),
};
static void test_choose(void)
{
usbdesc_pick p;
CHECK(usbdesc_choose(desc_310a, sizeof desc_310a, VID_8BITDO, &p) == USBPAD_XINPUT, "310a: not XInput");
CHECK(p.iface == 0 && p.ep_in == 0x84 && p.mps_in == 32, "310a: in %#x/%u iface %u", p.ep_in, p.mps_in, p.iface);
CHECK(p.ep_out == 0x05 && p.mps_out == 32, "310a: out %#x/%u", p.ep_out, p.mps_out);
CHECK(usbdesc_choose(desc_310a, sizeof desc_310a, 0x045E, &p) == USBPAD_XINPUT, "XInput of another vendor");
CHECK(usbdesc_choose(desc_hid, sizeof desc_hid, VID_8BITDO, &p) == USBPAD_8BITDO, "8BitDo HID: not chosen");
CHECK(p.ep_in == 0x84 && p.mps_in == 64, "8BitDo HID: in %#x/%u", p.ep_in, p.mps_in);
CHECK(usbdesc_choose(desc_hid, sizeof desc_hid, 0x1234, &p) == USBPAD_NONE, "HID of another vendor taken");
CHECK(usbdesc_choose(desc_kbd, sizeof desc_kbd, VID_8BITDO, &p) == USBPAD_NONE, "boot keyboard taken");
CHECK(usbdesc_choose(desc_310a, 20, VID_8BITDO, &p) == USBPAD_NONE, "truncated descriptor taken");
}
/* Xbox 360 report: 00 14, buttons, LT, RT, LX, LY, RX, RY (int16, y up) */
static void xreport(unsigned char r[20], unsigned b, int lt, int rt, int lx, int ly, int rx, int ry)
{
int i;
for (i = 0; i < 20; i++) r[i] = 0;
r[0] = 0x00;
r[1] = 0x14;
r[2] = (unsigned char)b;
r[3] = (unsigned char)(b >> 8);
r[4] = (unsigned char)lt;
r[5] = (unsigned char)rt;
r[6] = (unsigned char)lx; r[7] = (unsigned char)(lx >> 8);
r[8] = (unsigned char)ly; r[9] = (unsigned char)(ly >> 8);
r[10] = (unsigned char)rx; r[11] = (unsigned char)(rx >> 8);
r[12] = (unsigned char)ry; r[13] = (unsigned char)(ry >> 8);
}
static void xexpect(const char *what, unsigned b, uint32_t want)
{
unsigned char r[20];
pad_state st;
xreport(r, b, 0, 0, 0, 0, 0, 0);
if (!usbpad_parse_xinput(r, 20, &st)) {
printf("FAIL %s: not parsed\n", what);
fails++;
} else if (st.buttons != want) {
printf("FAIL %s: buttons %#x, want %#x\n", what, (unsigned)st.buttons, (unsigned)want);
fails++;
}
}
static void test_xinput(void)
{
unsigned char r[20];
pad_state st;
xexpect("idle", 0, 0);
xexpect("up", 0x0001, PAD_UP);
xexpect("down", 0x0002, PAD_DOWN);
xexpect("left", 0x0004, PAD_LEFT);
xexpect("right", 0x0008, PAD_RIGHT);
xexpect("Start", 0x0010, PAD_OPTIONS);
xexpect("Back", 0x0020, PAD_CREATE);
xexpect("L3", 0x0040, PAD_L3);
xexpect("R3", 0x0080, PAD_R3);
xexpect("LB", 0x0100, PAD_L1);
xexpect("RB", 0x0200, PAD_R1);
xexpect("Guide", 0x0400, PAD_PS);
xexpect("A", 0x1000, PAD_CROSS);
xexpect("B", 0x2000, PAD_CIRCLE);
xexpect("X", 0x4000, PAD_SQUARE);
xexpect("Y", 0x8000, PAD_TRIANGLE);
xreport(r, 0, 0, 0, 0, 0, 0, 0);
usbpad_parse_xinput(r, 20, &st);
CHECK(st.lx == 128 && st.ly == 127 && st.rx == 128 && st.ry == 127, "centre %u %u %u %u", st.lx, st.ly, st.rx, st.ry);
xreport(r, 0, 255, 30, -32768, 32767, 32767, -32768);
usbpad_parse_xinput(r, 20, &st);
CHECK(st.lx == 0 && st.ly == 0, "left stick up-left: %u %u", st.lx, st.ly);
CHECK(st.rx == 255 && st.ry == 255, "right stick down-right: %u %u", st.rx, st.ry);
CHECK(st.l2 == 255 && st.r2 == 30, "triggers %u %u", st.l2, st.r2);
CHECK((st.buttons & (PAD_L2 | PAD_R2)) == (PAD_L2 | PAD_R2), "trigger buttons %#x", (unsigned)st.buttons);
xreport(r, 0, 20, 0, 0, 0, 0, 0);
usbpad_parse_xinput(r, 20, &st);
CHECK(!(st.buttons & PAD_L2), "light LT pressed L2");
r[0] = 0x01; r[1] = 0x03; /* LED status, not input */
CHECK(!usbpad_parse_xinput(r, 3, &st), "LED status taken as input");
xreport(r, 0, 0, 0, 0, 0, 0, 0);
CHECK(!usbpad_parse_xinput(r, 13, &st), "short report taken");
}
static void test_string(void)
{
/* string descriptor: length, type 3, UTF-16LE */
static const unsigned char name[] = { 22, 3, '8', 0, 'B', 0, 'i', 0, 't', 0, 'D', 0, 'o', 0, ' ', 0,
'2', 0, 'C', 0, ' ', 0 };
static const unsigned char odd[] = { 8, 3, 'A', 0, 0xE9, 0, 0x3A, 0x26 };
char s[64];
CHECK(usbdesc_string(name, sizeof name, s, sizeof s) && !strcmp(s, "8BitDo 2C"), "string \"%s\"", s);
CHECK(usbdesc_string(odd, sizeof odd, s, sizeof s) && !strcmp(s, "A??"), "non-ASCII \"%s\"", s);
CHECK(usbdesc_string(name, sizeof name, s, 5) && !strcmp(s, "8Bit"), "cut \"%s\"", s);
CHECK(!usbdesc_string(name, 1, s, sizeof s), "1 byte taken");
CHECK(!usbdesc_string((const unsigned char[]){ 4, 2, 'A', 0 }, 4, s, sizeof s), "not a string taken");
CHECK(!usbdesc_string((const unsigned char[]){ 4, 3, ' ', 0 }, 4, s, sizeof s), "blank taken");
}
int main(void)
{
test_string();
test_choose();
test_xinput();
printf("%s (%d failure(s))\n", fails ? "FAILED" : "all passed", fails);
return fails != 0;
}
+42
View File
@@ -0,0 +1,42 @@
#!/bin/sh
# Installs the build tools and the PS5 payload SDK on macOS (Homebrew).
#
# Homebrew ships ld.lld in its own formula, outside the directory
# `llvm-config --bindir` names, where the SDK looks for every tool. So the
# tools are linked into one directory, with an llvm-config that names it,
# and the SDK finds that one through LLVM_CONFIG.
set -e
SDK_VERSION=v0.45
SDK_DIR="$HOME/.local/ps5-payload-sdk"
BIN="$HOME/.local/ps5-llvm/bin"
brew install llvm lld
LLVM_BIN="$(brew --prefix llvm)/bin"
LLD_BIN="$(brew --prefix lld)/bin"
mkdir -p "$BIN"
for t in clang clang++ clang-cpp llvm-ar llvm-nm llvm-objcopy llvm-ranlib llvm-strip; do
ln -sf "$LLVM_BIN/$t" "$BIN/$t"
done
ln -sf "$LLD_BIN/ld.lld" "$BIN/ld.lld"
cat > "$BIN/llvm-config" <<EOF
#!/bin/sh
[ "\$1" = "--bindir" ] && { echo "$BIN"; exit 0; }
exec "$LLVM_BIN/llvm-config" "\$@"
EOF
chmod +x "$BIN/llvm-config"
if [ ! -d "$SDK_DIR" ]; then
mkdir -p "$HOME/.local"
cd /tmp
curl -sSL -o ps5-payload-sdk.zip \
"https://github.com/ps5-payload-dev/sdk/releases/download/$SDK_VERSION/ps5-payload-sdk.zip"
unzip -q -o ps5-payload-sdk.zip -d "$HOME/.local"
rm ps5-payload-sdk.zip
fi
cat > "$HOME/.ps5sdk" <<EOF
export PS5_PAYLOAD_SDK="$SDK_DIR"
export LLVM_CONFIG="$BIN/llvm-config"
EOF
echo "ok: . ~/.ps5sdk && make"