mirror of
https://github.com/sub0x00001/pad2c.git
synced 2026-10-24 00:02:01 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e243fa093 | ||
|
|
6de27e3aea | ||
|
|
7f2f0822cf | ||
|
|
a41f640b7b | ||
|
|
25138ce0ab |
No files matched your search
@@ -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.
|
||||
|
||||
```
|
||||
```
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
<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
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
Executable
+42
@@ -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"
|
||||
Reference in new issue
Block a user