1 Commits
Author SHA1 Message Date
Sub 41bece1b31 padbridge 1.3.0: rumble in PS4 games, Switch Pro Controller, View + Menu as the touchpad (#6)
* Rumble research: log what the console asks of each virtual pad

scePadVirtualDeviceGetRemoteSetting read every 16 ms per live pad and
logged when it changes (at most 20 lines a second). On the PS5 with a
virtual DualSense: [0] update flag, [5..7] light bar RGB (Hades asked
for ba 01 13), [56] a user is assigned. [3] and [4], the motors on a
PS4, stayed 0 in Hades and Hollow Knight (PS5 games). Not for release.

* Rumble: what the console asks of a virtual pad reaches XInput pads

scePadVirtualDeviceGetRemoteSetting, read every 16 ms per live pad,
holds the motors as a DualShock 4 output report does: [3] the large
motor, [4] the small one (seen in Crash Bandicoot N. Sane Trilogy, a PS4
game: 23/47/6b/8e ramps on [3], 3f/8c on [4]). PS5 games (Hades, Hollow
Knight) send DualSense haptics as audio instead and leave them at 0.

- usbpad_rumble(): XInput rumble (00 08 00 <large> <small> 00 00 00) on
  the OUT endpoint, started without waiting; a newer value replaces one
  not sent yet.
- Rumble stopped when the console turns a pad off and when padbridge
  stops, so no controller is left rumbling.
- The research log now covers all 256 bytes.

Tested on the PS5: the Ultimate 2C on its dongle in XInput mode rumbles
on Crash's TNT/Nitro boxes and deaths.

* View + Menu as the touchpad on XInput pads; research logs

XInput pads have no touchpad and no spare button for it: the Ultimate 2C
sends no L4/R4 in XInput mode, and its Star and Mapping buttons never
reach the console in either mode (checked on raw reports). Pressing View
and Menu together is now the touchpad click on XInput pads.

- src/combo.c: View or Menu alone is held back for 60 ms, so the console
  does not see a Create press on the way to the touchpad; held longer
  they go through as they are, and a shorter tap is sent when it ends.
  The touchpad stays until both are released. tests/test_combo.c.
- The settings page shows what the virtual pad is given (the touchpad
  lit, not View and Menu).
- Research only, to be trimmed before a release: raw XInput reports are
  logged when they change.

The 2C receiver in DInput mode does not rumble: report 1 (8 bytes) and
the 8BitDo-style 05 report, by interrupt OUT and by SET_REPORT, are
accepted and ignored; 8BitDo documents rumble for XInput only.

Tested on the PS5: View + Menu lights the touchpad, each alone stays
Create or Options; XInput and DInput.

* padbridge 1.3.0: rumble in PS4 games, View + Menu as the touchpad

- Rumble: the large and small motors a game asks of the virtual
  DualSense (scePadVirtualDeviceGetRemoteSetting [3], [4]) go to
  controllers in XInput mode as XInput rumble. PS4 games fill them; PS5
  games turn rumble into DualSense haptics inside the console and leave
  them at 0. The log says when a controller's rumble starts and stops,
  and whether that controller can rumble.
- The research logging (all remote setting bytes, raw XInput reports)
  is gone.
- The log line for a device padbridge does not read says so plainly
  ("not read by padbridge"): a DualSense on its cable is one.
- README: a Vibration section, the 2C's modes with what each gives,
  Star and Mapping, View + Menu in the button map, the 60 ms wait.
- Version 1.3.0, CHANGELOG.

Tested on the PS5: the Ultimate 2C in XInput rumbles in Crash Bandicoot
N. Sane Trilogy; View + Menu lights the touchpad.

* Switch Pro Controller over USB: the adapter's Switch mode, with rumble

The 8BitDo USB Wireless Adapter 2 passes rumble on in XInput mode to
PlayStation controllers only. In its Switch mode it is a Switch Pro
Controller (057e:2009) and passes rumble to Switch-type controllers (a
GameSir G8 in Switch mode, Switch Pro, Joy-Con). As documented by
dekuNukem's Switch reverse engineering notes and done by SDL:

- usbdesc: 057e:2009's HID interface is USBPAD_SWITCH (the choice now
  takes the product id).
- usbpad: at connection the USB handshake (80 02, 80 03, 80 02, 80 04)
  and subcommands on report 01: full input report 0x30, vibration on,
  player 1's light; each with a 100 ms wait and three tries. Without it
  the pad only sends a status and enumerates again every few seconds.
- mapswitch.c: report 0x30 (and 0x21) by position, as on a DualSense:
  B cross, A circle, Y square, X triangle, ZL/ZR as full L2/R2, Capture
  as the touchpad; sticks centred on the first report, reach learnt.
  Rumble as SDL encodes it (its tables, zlib licence), report 0x10, sent
  again every 50 ms while on.
- Fixed before it shipped: the mode name looked up past the end of its
  table for the new kind and crashed; the lookup is now guarded.
- tests/test_switch.c.

Tested on the PS5: the adapter locked in Switch mode with a GameSir G8
(Switch mode): setup answered in 150 ms; buttons, sticks, Capture as
the touchpad; rumble in Crash Bandicoot N. Sane Trilogy. The G8 sends
its buttons by label, so it needs the Nintendo layout switch.

* README and CHANGELOG: the adapter's Switch mode, rumble per controller, credits

* README, CHANGELOG: a DualSense through the adapter rumbles (tested)

* Switch rumble: felt on third-party pads too

Crash Bandicoot's spin asks the small motor alone at 63 (25%). Through
the adapter's Switch mode the GameSir G8 felt nothing: the small motor
went to the high band, which it hardly feels, and 25% is below what its
motor starts at. Now, for Switch Pro pads only:

- the low band takes the stronger of the two motors;
- any rumble is lifted to start at 100 (about 40%), up to 255
  (switch_rumble_boost).

Tested on the PS5: the G8 through the adapter in Switch mode rumbles
lightly on each spin, and harder on boxes and enemies.

* README: the GameSir G8 through the adapter (its Switch mode, Nintendo layout, rumble)
2026-10-23 11:40:36 -03:00
18 changed files with 881 additions and 38 deletions

No files matched your search

+19
View File
@@ -1,5 +1,24 @@
# Changelog
## 1.3.0
- **Vibration in PS4 games**, on controllers in XInput mode and Switch Pro controllers: padbridge reads what the game asks of the virtual
DualSense (`scePadVirtualDeviceGetRemoteSetting`, large and small motors as on a DualShock 4) every 16 ms and sends
it as XInput rumble. Tested with Crash Bandicoot N. Sane Trilogy: the Ultimate 2C in XInput mode, and a DualSense
through the USB Wireless Adapter 2 in XInput mode. Rumble stops
when the console turns a controller off and when padbridge stops. The log says when a controller's rumble starts
and stops.
- **Switch Pro Controller over USB** (`057e:2009`): the USB Wireless Adapter 2 in its **Switch mode** (- + L), with
**vibration** for Switch-type controllers paired with it (tested: GameSir G8 Plus Galileo in Switch mode). The
adapter passes rumble in XInput mode to PlayStation controllers only. padbridge does the USB handshake and
subcommands at connection, reads report 0x30 by position (Capture as the touchpad), and sends HD Rumble encoded
as SDL does, with two changes for third-party pads, which hardly feel weak or high-band rumble: the low band takes
the stronger of the two motors, and any rumble starts at 40% (Crash Bandicoot's spin asks 25%).
- **View + Menu together as the touchpad** on XInput controllers, which have no touchpad and, for the Ultimate 2C,
no L4/R4 in that mode. View or Menu alone waits 60 ms for the other, so the touchpad never opens the Create menu.
- Not possible, found out: PS5 games do not rumble (the console turns their requests into DualSense haptic audio
before the virtual DualSense sees them); the Ultimate 2C does not rumble in DInput mode (8BitDo documents rumble
for XInput only); its Star and Mapping buttons never reach the console.
## 1.2.0
- **pad2c is now padbridge**: it is no longer about one controller. The payloads are `padbridge-<version>.elf` and
`padbridge-probe.elf`, notifications read "padbridge by Sub", and its files live in `/data/padbridge`. On its first
+5 -1
View File
@@ -17,6 +17,10 @@ test:
./build/test_config
cc -std=c11 -Wall -Wextra -Werror -D_DEFAULT_SOURCE -Isrc -o build/test_web src/web.c src/log.c src/util.c tests/test_web.c
./build/test_web
cc -std=c11 -Wall -Wextra -Werror -Isrc -o build/test_combo src/combo.c tests/test_combo.c
./build/test_combo
cc -std=c11 -Wall -Wextra -Werror -Isrc -o build/test_switch src/mapswitch.c src/usbdesc.c tests/test_switch.c
./build/test_switch
ifdef PS5_PAYLOAD_SDK
include $(PS5_PAYLOAD_SDK)/toolchain/prospero.mk
@@ -25,7 +29,7 @@ endif
CFLAGS := -std=gnu11 -Wall -Wextra -O2 -Isrc
LDLIBS := -lScePad -lSceUserService -ldl
SRCS := src/main.c src/usbpad.c src/usbdesc.c src/map8bitdo.c src/mapxinput.c src/config.c src/web.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/mapswitch.c src/config.c src/combo.c src/web.c src/ps5_vpad.c src/ps5_power.c src/lock.c src/log.c src/util.c
dist/padbridge-$(VERSION).elf: $(SRCS) $(wildcard src/*.h)
@mkdir -p dist
+61 -10
View File
@@ -33,26 +33,43 @@ controller with it, switch the controller on: nothing to set on the adapter.
| Controller | How it pairs with the adapter | Result |
|---|---|---|
| DualSense | its normal pairing (Create + PS) | ✅ all buttons, touchpad click (DInput mode) |
| DualSense | its normal pairing (Create + PS) | ✅ all buttons; vibration in XInput mode; touchpad click in DInput mode (View + Menu in XInput) |
| 8BitDo Ultimate 2C Wireless | Switch mode (Home + Y) | ✅ all buttons |
| GameSir G8 Plus Galileo | Switch mode (its only mode the adapter accepts) | ✅ with **Nintendo layout** on (it sends A/B and X/Y swapped) |
| GameSir G8 Plus Galileo | Switch mode (its only mode the adapter accepts) | ✅ with **Nintendo layout** on (it sends A/B and X/Y by label); **vibration** with the adapter in its Switch mode |
| Anything else the adapter accepts (DualShock 4, Xbox One / Series, Switch Pro, Joy-Con, Wii U Pro, 8BitDo) | | expected to work, untested: please report |
### 8BitDo Ultimate 2C Wireless on its own dongle
| Connection | Result |
|---|---|
| 2.4G dongle, DInput mode (`2dc8:301c`) | ✅ all buttons, L4/R4 as the touchpad click |
| 2.4G dongle, XInput mode (`2dc8:310a`) | ✅ all buttons except L4/R4 (the controller does not send them in this mode) |
| USB cable (`2dc8:310a`, XInput) | ✅ all buttons except L4/R4 |
| 2.4G dongle, DInput mode (`2dc8:301c`) | ✅ all buttons, L4/R4 as the touchpad click; no vibration (the controller only rumbles in XInput) |
| 2.4G dongle, XInput mode (`2dc8:310a`) | ✅ all buttons, **vibration** in PS4 games, View + Menu as the touchpad (the controller sends no L4/R4 in this mode) |
| USB cable (`2dc8:310a`, XInput) | ✅ as XInput mode above |
The 8BitDo Ultimate 2C **Bluetooth** Controller (the Switch edition, without a dongle) is a different product and is
not supported directly; it works through the USB Wireless Adapter 2.
The Ultimate 2C's **Star** and **Mapping** buttons never reach the console, in either mode: the controller keeps
them for its own turbo and mapping settings.
### Vibration
**PS4 games** rumble the controller (tested: Crash Bandicoot N. Sane Trilogy). padbridge reads what the game asks of
the virtual DualSense (`scePadVirtualDeviceGetRemoteSetting`: the large and small motors, as on a DualShock 4) and
sends it on:
| Controller | Rumbles |
|---|---|
| Ultimate 2C on its dongle | in XInput mode (tested) |
| Through the USB Wireless Adapter 2, Switch-type controllers (GameSir G8 tested, Switch Pro, Joy-Con) | with the adapter in its **Switch mode** (tested) |
| Through the USB Wireless Adapter 2, PlayStation controllers (DualSense tested) | in XInput mode |
**PS5 games do not rumble**: they send DualSense haptics, and the console turns even their classic rumble requests
into haptic audio before the virtual DualSense sees them. Controllers in DInput mode do not rumble either.
### Not yet
Vibration, light bar, motion sensors, audio. PS5 games send DualSense haptics as audio to the controller, and a
virtual DualSense has no known way to receive them yet.
Vibration in PS5 games, light bar, motion sensors, audio.
## Install
@@ -87,14 +104,34 @@ mode, so the console's Accessories list shows only the controller.
| Mode | How | Notes |
|---|---|---|
| XInput (`2dc8:3106`) | automatic | everything but a DualSense's touchpad |
| XInput (`2dc8:3106`) | automatic | vibration in PS4 games for PlayStation controllers (DualSense tested); View + Menu as the touchpad |
| DInput (`2dc8:3105`) | hold **View + Left** (Create + Left on a DualSense) for 3 s; the light blinks twice | passes a DualSense's touchpad click |
| Switch (`057e:2009`, a Switch Pro Controller) | hold **- + L** (View + LB) for 3 s; the light blinks twice | **vibration for Switch-type controllers** (the adapter passes it on only in this mode); Capture as the touchpad; buttons by position, so a pad that sends them by label (GameSir G8) needs Nintendo layout |
| back to automatic | hold the adapter's button until its light goes off, then pair the controller again | |
| DualShock 4 (`054c:05c4`) | where it ends on a PS5 without padbridge's answer | does not work: it stops sending after a moment |
Pairing a DualSense with the adapter unpairs it from the console: plug it into the console with its cable and press
PS to pair it back. The adapter takes one controller at a time.
## The GameSir G8 Plus Galileo through the adapter
The G8's Bluetooth modes are Android, Switch and G-Touch; the USB Wireless Adapter 2 accepts only its **Switch**
mode. It then works in both of the adapter's modes, with two things to know:
| Adapter mode | Buttons | Vibration |
|---|---|---|
| **Switch** (hold **- + L** for 3 s) | tick **Nintendo layout** on the "Pro Controller" card of the settings page | ✅ (PS4 games) |
| XInput (automatic) | tick **Nintendo layout** on the "8BitDo USB Wireless Adapter 2" card | ❌ the adapter passes rumble in XInput to PlayStation controllers only |
- **Nintendo layout:** in Switch mode the G8 sends its buttons by their printed letter (its bottom button is "A",
which a Switch reads as the right one), not by position. A genuine Switch Pro Controller needs no setting.
- **Weak rumble:** simple rumble motors like the G8's do not move below a certain strength, and the Switch format
splits rumble into a low and a high band, of which such pads feel mostly the low one. In the Switch mode,
padbridge sends the stronger motor to the low band and starts any rumble at about 40%, so a light request (Crash
Bandicoot's spin asks 25%) is still felt.
- The setting is kept per card, so the G8 and a DualSense can share the adapter: the DualSense uses the adapter's
XInput mode, without Nintendo layout.
## The Ultimate 2C's two dongle modes
The controller keeps the last mode chosen. With the controller on:
@@ -112,12 +149,16 @@ The controller keeps the last mode chosen. With the controller on:
| Home / Guide | PS |
| L3 / R3 | L3 / R3 |
| Ultimate 2C L4 / R4 (DInput), DualSense touchpad (adapter, DInput) | touchpad click |
| View + Menu pressed together, on XInput controllers | touchpad click |
| Switch Pro (and the adapter's Switch mode): B / A / Y / X, L / R, ZL / ZR, - / +, Home, Capture | ✕ / ○ / □ / △ (by position), L1 / R1, L2 / R2, Create / Options, PS, touchpad click |
## Notes
- "Turn off" in the console's menu disconnects the controller from the console, as with a DualSense, and Home
connects it again. The controller itself stays on until it goes to sleep by itself.
- Every notification reads "padbridge by Sub", then the controller, its mode and what happened.
- On XInput controllers, View or Menu pressed alone reaches the console 60 ms late: padbridge waits that long for
the other one, so that View + Menu (the touchpad) does not open the Create menu on the way.
## Files
@@ -162,8 +203,13 @@ not read, with its id and name: a controller in another mode (Switch, DS4) shows
its own virtual DualSense. New devices are looked for every 100 ms. The console's own HID driver holds the
interface but ignores the device. The internal Bluetooth chip (`ugen0.2`) is never touched.
- `src/usbdesc.c` picks the interface from the USB descriptor: XInput (`ff/5d/01`) from any vendor first, then an
8BitDo HID interface, never a keyboard. `src/map8bitdo.c` (8BitDo DInput report 1) and `src/mapxinput.c` (Xbox 360
report) turn the reports into DualSense buttons.
Switch Pro Controller (`057e:2009`), then an 8BitDo HID interface, never a keyboard. `src/map8bitdo.c` (8BitDo
DInput report 1), `src/mapxinput.c` (Xbox 360 report) and `src/mapswitch.c` (Switch Pro report 0x30) turn the
reports into DualSense buttons.
- A Switch Pro Controller gets the USB handshake (80 02, 80 03, 80 02, 80 04) and its subcommands (full reports,
vibration, player light) at connection; without them it keeps enumerating again. Its rumble is HD Rumble, encoded
as SDL does and sent again every 50 ms while on.
- Rumble: `scePadVirtualDeviceGetRemoteSetting` is read every 16 ms per virtual pad (motors at [3] and [4]).
- XInput devices get what Windows sends first (the Microsoft OS string and the XUSB vendor request, then the LED):
the USB Wireless Adapter 2 keeps XInput when its host answers like that. A boot keyboard or mouse beside the pad is
detached from the console's driver.
@@ -191,6 +237,11 @@ make # dist/padbridge-<version>.elf and dist/padbridge-probe.elf
- The virtual DualSense code (`ps5_vpad.c`) and the helpers `log.c`, `util.c`, `lock.c`, `ps5_power.c` and `pad.h`
come from **AnyPad PS5** by **@elmonomalvad0** (sinfiltros), GPL-3.0-or-later, adapted here.
- [ps5-payload-sdk](https://github.com/ps5-payload-dev/sdk) by John Törnblom.
- The Switch Pro Controller's USB protocol and HD Rumble: [dekuNukem's Nintendo Switch reverse engineering
notes](https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering), and [SDL](https://github.com/libsdl-org/SDL)'s
HIDAPI Switch driver, whose rumble tables `src/mapswitch.c` uses (zlib licence).
- [Control4Free](https://github.com/MoHadiShibli/Control4Free) (GPL-3.0), whose notes showed that
`scePadVirtualDeviceGetRemoteSetting` returns what a game asks of a virtual pad.
## Licence
+53
View File
@@ -0,0 +1,53 @@
#include "combo.h"
#define VM (PAD_CREATE | PAD_OPTIONS)
void combo_reset(combo *c)
{
c->since = -1;
c->held = 0;
c->tap_until = 0;
c->tap = 0;
c->on = 0;
}
uint32_t combo_filter(combo *c, uint32_t buttons, long now)
{
uint32_t vm = buttons & VM, rest = buttons & ~(uint32_t)VM;
if (c->on) {
if (!vm) c->on = 0;
else return rest | PAD_TOUCHPAD;
}
if (vm == VM && c->since >= 0 && now - c->since < COMBO_WINDOW_MS) {
c->on = 1; /* the second one came in time */
c->since = -1;
c->held = 0;
return rest | PAD_TOUCHPAD;
}
if (vm == VM && c->since < 0 && !c->held && !c->tap) {
c->on = 1; /* both in the same report */
return rest | PAD_TOUCHPAD;
}
if (vm && vm != VM) {
if (c->since < 0 && !c->held) {
c->since = now;
c->held = vm;
}
if (c->held && now - c->since < COMBO_WINDOW_MS) return rest; /* held back */
c->held = 0; /* held past the window: as it is */
return buttons;
}
if (!vm && c->held) { /* a tap shorter than the window */
c->tap = c->held;
c->tap_until = now + COMBO_TAP_MS;
c->held = 0;
c->since = -1;
}
if (!vm) c->since = -1;
if (c->tap) {
if (now < c->tap_until) return rest | c->tap;
c->tap = 0;
}
return buttons;
}
+31
View File
@@ -0,0 +1,31 @@
/* View + Menu (Create + Options) pressed together as the touchpad click, for
* pads that have no touchpad and no spare button to stand for it: XInput
* pads (the Ultimate 2C in XInput mode sends no L4/R4; Star and Mapping never
* reach the console in either mode).
*
* Nobody presses two buttons in the same millisecond: View or Menu alone is
* held back for COMBO_WINDOW_MS, so that the console does not see a Create
* press on the way to the touchpad (it would open the Create menu). Held
* longer, or with the other one late, they go through as they are; a tap
* shorter than the window is sent when it ends, for COMBO_TAP_MS. Once the
* touchpad is on, it stays until both are released. */
#ifndef PADBRIDGE_COMBO_H
#define PADBRIDGE_COMBO_H
#include "pad.h"
#define COMBO_WINDOW_MS 60
#define COMBO_TAP_MS 60
typedef struct {
long since; /* ms: View or Menu alone since then, -1 if not */
uint32_t held; /* which of them is being held back */
long tap_until; /* ms: a short tap being sent until then */
uint32_t tap;
int on; /* the touchpad is on */
} combo;
void combo_reset(combo *c);
uint32_t combo_filter(combo *c, uint32_t buttons, long now);
#endif
+64 -6
View File
@@ -22,6 +22,7 @@
#include "lock.h"
#include "log.h"
#include "ps5_power.h"
#include "combo.h"
#include "config.h"
#include "ps5_vpad.h"
#include "usbpad.h"
@@ -68,12 +69,42 @@ typedef struct {
int ps_latch; /* PS (Home) held since before: not a new press */
char model[80]; /* "<name> (<mode>)", for the notifications */
int swap; /* A/B and X/Y exchanged (config.h) */
int xinput; /* View + Menu as the touchpad (combo.h) */
combo combo;
pad_state sent; /* what the virtual pad was last given */
long remote_at; /* ms: rumble last read */
int rumbling;
char user[40]; /* the console user's name, for the settings page */
long user_at; /* ms: when it was last looked up */
} ctl;
static ctl g_ctl[HOST_MAX_PADS];
/* Rumble: what the console asks of each live virtual pad, read every 16 ms
* (games pulse rumble for tens of milliseconds). It is kept as a DualShock 4
* output report: [3] large motor, [4] small motor. PS4 games fill them; PS5
* games turn rumble into DualSense haptics inside the system and leave them
* at 0. Logged when a pad's rumble starts and stops. */
#define T_REMOTE 16
static void remote_poll(int slot, long now)
{
ctl *c = &g_ctl[slot];
unsigned char buf[256];
int on;
if (!c->vpad_made || now - c->remote_at < T_REMOTE) return;
c->remote_at = now;
if (vpad_remote_setting(slot, buf, sizeof buf) != 0) return;
usbpad_rumble(c->pad, buf[3], buf[4]);
on = buf[3] || buf[4];
if (on != c->rumbling) {
c->rumbling = on;
log_line("slot %d: rumble %s%s", slot, on ? "on" : "off",
on && !usbpad_can_rumble(c->pad) ? " (this pad cannot rumble)" : "");
}
}
/* From the settings page. */
static void set_swap(int slot, int on)
{
@@ -106,10 +137,22 @@ static void web_fill(web_pad *w, long now)
snprintf(w[i].mode, sizeof w[i].mode, "%s", usbpad_mode(c->pad));
snprintf(w[i].user, sizeof w[i].user, "%s", c->user);
w[i].swap = c->swap;
w[i].st = c->st;
w[i].st = c->vpad_made ? c->sent : c->st;
}
}
/* The pad's state to its virtual DualSense, View + Menu as the touchpad on
* XInput pads. */
static void ctl_send(int slot, long now)
{
ctl *c = &g_ctl[slot];
c->sent = c->st;
if (c->xinput) c->sent.buttons = combo_filter(&c->combo, c->st.buttons, now);
vpad_update(slot, &c->sent);
c->last_sent = now;
}
static void ctl_poll(int slot, long now)
{
ctl *c = &g_ctl[slot];
@@ -147,11 +190,10 @@ static void ctl_poll(int slot, long now)
notify("%s connected", c->model);
}
if (c->ps_latch) c->st.buttons &= ~(uint32_t)PAD_PS;
if (c->vpad_made) vpad_update(slot, &c->st);
c->last_sent = now;
} else if (c->vpad_made && now - c->last_sent >= T_RESEND) {
vpad_update(slot, &c->st); /* keep the pad fed while nothing changes */
if (c->vpad_made) ctl_send(slot, now);
c->last_sent = now;
} else if (c->vpad_made && (now - c->last_sent >= T_RESEND || (c->xinput && c->combo.held))) {
ctl_send(slot, now); /* keep the pad fed; let a held-back View or Menu out */
}
}
@@ -232,14 +274,20 @@ int main(void)
if ((g_ctl[i].pad = usbpad_open())) {
g_ctl[i].turned_off = 0;
g_ctl[i].swap = config_swap(usbpad_name(g_ctl[i].pad));
g_ctl[i].xinput = !strcmp(usbpad_mode(g_ctl[i].pad), "XInput");
combo_reset(&g_ctl[i].combo);
g_ctl[i].user_at = -10000;
g_ctl[i].rumbling = 0;
log_line("slot %d: %s%s", i, usbpad_name(g_ctl[i].pad), g_ctl[i].swap ? ", A/B and X/Y swapped" : "");
}
break;
}
}
for (i = 0; i < HOST_MAX_PADS; i++)
if (g_ctl[i].pad) ctl_poll(i, now);
if (g_ctl[i].pad) {
ctl_poll(i, now);
remote_poll(i, now);
}
vpad_poll(now);
{
web_pad w[HOST_MAX_PADS];
@@ -253,6 +301,7 @@ int main(void)
ctl *c = &g_ctl[i];
if (!c->vpad_made || !vpad_off(i)) continue;
vpad_remove(i);
usbpad_rumble(c->pad, 0, 0);
c->vpad_made = 0;
c->turned_off = 1;
c->ps_latch = (c->st.buttons & PAD_PS) != 0;
@@ -281,6 +330,15 @@ int main(void)
log_line("stopping: %s", why);
for (i = 0; i < HOST_MAX_PADS; i++) {
if (g_ctl[i].vpad_made) vpad_remove(i);
if (g_ctl[i].pad && usbpad_can_rumble(g_ctl[i].pad)) {
pad_state st;
int k;
usbpad_rumble(g_ctl[i].pad, 0, 0); /* no pad left rumbling */
for (k = 0; k < 10; k++) {
usbpad_poll(g_ctl[i].pad, &st);
usleep(2000);
}
}
usbpad_close(g_ctl[i].pad);
}
web_stop();
+157
View File
@@ -0,0 +1,157 @@
#include "mapswitch.h"
#include <stddef.h>
#include <string.h>
void switch_sticks_reset(switch_sticks *c)
{
int i;
c->set = 0;
for (i = 0; i < 4; i++) {
c->centre[i] = 2048;
c->reach[i] = SWITCH_STICK_REACH;
}
}
/* A 12-bit stick value to 0..255, 128 at the centre. `up` flips it (the
* DualSense's y grows downwards). */
static uint8_t axis(switch_sticks *c, int i, int v, int up)
{
int d = up ? c->centre[i] - v : v - c->centre[i], a = d < 0 ? -d : d, out;
if (a > c->reach[i]) c->reach[i] = a;
out = d < 0 ? 128 - (a * 128 + c->reach[i] / 2) / c->reach[i] : 128 + (a * 127 + c->reach[i] / 2) / c->reach[i];
return (uint8_t)(out < 0 ? 0 : out > 255 ? 255 : out);
}
int switch_parse(const unsigned char *r, int n, switch_sticks *cal, pad_state *st)
{
static const struct { int byte; uint8_t bit; uint32_t pad; } map[] = {
{ 3, 0x01, PAD_SQUARE }, { 3, 0x02, PAD_TRIANGLE }, { 3, 0x04, PAD_CROSS }, { 3, 0x08, PAD_CIRCLE },
{ 3, 0x40, PAD_R1 }, { 3, 0x80, PAD_R2 }, { 4, 0x01, PAD_CREATE }, { 4, 0x02, PAD_OPTIONS },
{ 4, 0x04, PAD_R3 }, { 4, 0x08, PAD_L3 }, { 4, 0x10, PAD_PS }, { 4, 0x20, PAD_TOUCHPAD },
{ 5, 0x01, PAD_DOWN }, { 5, 0x02, PAD_UP }, { 5, 0x04, PAD_RIGHT }, { 5, 0x08, PAD_LEFT },
{ 5, 0x40, PAD_L1 }, { 5, 0x80, PAD_L2 },
};
int v[4];
size_t i;
if (n < 12 || (r[0] != 0x30 && r[0] != 0x21)) return 0;
v[0] = r[6] | (r[7] & 0x0F) << 8;
v[1] = r[7] >> 4 | r[8] << 4;
v[2] = r[9] | (r[10] & 0x0F) << 8;
v[3] = r[10] >> 4 | r[11] << 4;
if (!cal->set) {
for (i = 0; i < 4; i++) cal->centre[i] = v[i];
cal->set = 1;
}
pad_state_reset(st);
for (i = 0; i < sizeof map / sizeof map[0]; i++)
if (r[map[i].byte] & map[i].bit) st->buttons |= map[i].pad;
st->l2 = st->buttons & PAD_L2 ? 255 : 0; /* ZL and ZR are digital */
st->r2 = st->buttons & PAD_R2 ? 255 : 0;
st->lx = axis(cal, 0, v[0], 0);
st->ly = axis(cal, 1, v[1], 1);
st->rx = axis(cal, 2, v[2], 0);
st->ry = axis(cal, 3, v[3], 1);
return 1;
}
/* SDL's tables (SDL_hidapi_switch.c, zlib licence), after dekuNukem's
* rumble_data_table.md: an amplitude 0..65535 to the high band's byte and
* the low band's 9-bit value. */
static uint8_t high_amp(uint16_t a)
{
static const uint16_t t[101][2] = {
{ 0, 0x0 }, { 514, 0x2 }, { 775, 0x4 }, { 921, 0x6 }, { 1096, 0x8 }, { 1303, 0x0a }, { 1550, 0x0c },
{ 1843, 0x0e }, { 2192, 0x10 }, { 2606, 0x12 }, { 3100, 0x14 }, { 3686, 0x16 }, { 4383, 0x18 },
{ 5213, 0x1a }, { 6199, 0x1c }, { 7372, 0x1e }, { 7698, 0x20 }, { 8039, 0x22 }, { 8395, 0x24 },
{ 8767, 0x26 }, { 9155, 0x28 }, { 9560, 0x2a }, { 9984, 0x2c }, { 10426, 0x2e }, { 10887, 0x30 },
{ 11369, 0x32 }, { 11873, 0x34 }, { 12398, 0x36 }, { 12947, 0x38 }, { 13520, 0x3a }, { 14119, 0x3c },
{ 14744, 0x3e }, { 15067, 0x40 }, { 15397, 0x42 }, { 15734, 0x44 }, { 16079, 0x46 }, { 16431, 0x48 },
{ 16790, 0x4a }, { 17158, 0x4c }, { 17534, 0x4e }, { 17918, 0x50 }, { 18310, 0x52 }, { 18711, 0x54 },
{ 19121, 0x56 }, { 19540, 0x58 }, { 19967, 0x5a }, { 20405, 0x5c }, { 20851, 0x5e }, { 21308, 0x60 },
{ 21775, 0x62 }, { 22251, 0x64 }, { 22739, 0x66 }, { 23236, 0x68 }, { 23745, 0x6a }, { 24265, 0x6c },
{ 24797, 0x6e }, { 25340, 0x70 }, { 25894, 0x72 }, { 26462, 0x74 }, { 27041, 0x76 }, { 27633, 0x78 },
{ 28238, 0x7a }, { 28856, 0x7c }, { 29488, 0x7e }, { 30134, 0x80 }, { 30794, 0x82 }, { 31468, 0x84 },
{ 32157, 0x86 }, { 32861, 0x88 }, { 33581, 0x8a }, { 34316, 0x8c }, { 35068, 0x8e }, { 35836, 0x90 },
{ 36620, 0x92 }, { 37422, 0x94 }, { 38242, 0x96 }, { 39079, 0x98 }, { 39935, 0x9a }, { 40809, 0x9c },
{ 41703, 0x9e }, { 42616, 0xa0 }, { 43549, 0xa2 }, { 44503, 0xa4 }, { 45477, 0xa6 }, { 46473, 0xa8 },
{ 47491, 0xaa }, { 48531, 0xac }, { 49593, 0xae }, { 50679, 0xb0 }, { 51789, 0xb2 }, { 52923, 0xb4 },
{ 54082, 0xb6 }, { 55266, 0xb8 }, { 56476, 0xba }, { 57713, 0xbc }, { 58977, 0xbe }, { 60268, 0xc0 },
{ 61588, 0xc2 }, { 62936, 0xc4 }, { 64315, 0xc6 }, { 65535, 0xc8 },
};
int i;
for (i = 0; i < 101; i++)
if (a <= t[i][0]) return (uint8_t)t[i][1];
return (uint8_t)t[100][1];
}
static uint16_t low_amp(uint16_t a)
{
static const uint16_t t[101][2] = {
{ 0, 0x0040 }, { 514, 0x8040 }, { 775, 0x0041 }, { 921, 0x8041 }, { 1096, 0x0042 }, { 1303, 0x8042 },
{ 1550, 0x0043 }, { 1843, 0x8043 }, { 2192, 0x0044 }, { 2606, 0x8044 }, { 3100, 0x0045 },
{ 3686, 0x8045 }, { 4383, 0x0046 }, { 5213, 0x8046 }, { 6199, 0x0047 }, { 7372, 0x8047 },
{ 7698, 0x0048 }, { 8039, 0x8048 }, { 8395, 0x0049 }, { 8767, 0x8049 }, { 9155, 0x004a },
{ 9560, 0x804a }, { 9984, 0x004b }, { 10426, 0x804b }, { 10887, 0x004c }, { 11369, 0x804c },
{ 11873, 0x004d }, { 12398, 0x804d }, { 12947, 0x004e }, { 13520, 0x804e }, { 14119, 0x004f },
{ 14744, 0x804f }, { 15067, 0x0050 }, { 15397, 0x8050 }, { 15734, 0x0051 }, { 16079, 0x8051 },
{ 16431, 0x0052 }, { 16790, 0x8052 }, { 17158, 0x0053 }, { 17534, 0x8053 }, { 17918, 0x0054 },
{ 18310, 0x8054 }, { 18711, 0x0055 }, { 19121, 0x8055 }, { 19540, 0x0056 }, { 19967, 0x8056 },
{ 20405, 0x0057 }, { 20851, 0x8057 }, { 21308, 0x0058 }, { 21775, 0x8058 }, { 22251, 0x0059 },
{ 22739, 0x8059 }, { 23236, 0x005a }, { 23745, 0x805a }, { 24265, 0x005b }, { 24797, 0x805b },
{ 25340, 0x005c }, { 25894, 0x805c }, { 26462, 0x005d }, { 27041, 0x805d }, { 27633, 0x005e },
{ 28238, 0x805e }, { 28856, 0x005f }, { 29488, 0x805f }, { 30134, 0x0060 }, { 30794, 0x8060 },
{ 31468, 0x0061 }, { 32157, 0x8061 }, { 32861, 0x0062 }, { 33581, 0x8062 }, { 34316, 0x0063 },
{ 35068, 0x8063 }, { 35836, 0x0064 }, { 36620, 0x8064 }, { 37422, 0x0065 }, { 38242, 0x8065 },
{ 39079, 0x0066 }, { 39935, 0x8066 }, { 40809, 0x0067 }, { 41703, 0x8067 }, { 42616, 0x0068 },
{ 43549, 0x8068 }, { 44503, 0x0069 }, { 45477, 0x8069 }, { 46473, 0x006a }, { 47491, 0x806a },
{ 48531, 0x006b }, { 49593, 0x806b }, { 50679, 0x006c }, { 51789, 0x806c }, { 52923, 0x006d },
{ 54082, 0x806d }, { 55266, 0x006e }, { 56476, 0x806e }, { 57713, 0x006f }, { 58977, 0x806f },
{ 60268, 0x0070 }, { 61588, 0x8070 }, { 62936, 0x0071 }, { 64315, 0x8071 }, { 65535, 0x0072 },
};
int i;
for (i = 0; i < 101; i++)
if (a <= t[i][0]) return t[i][1];
return t[100][1];
}
uint8_t switch_rumble_boost(uint8_t v)
{
return v ? (uint8_t)(SWITCH_RUMBLE_FLOOR + (v * (255 - SWITCH_RUMBLE_FLOOR) + 127) / 255) : 0;
}
void switch_rumble_encode(uint8_t large, uint8_t small, unsigned char out[8])
{
static const unsigned char neutral[4] = { 0x00, 0x01, 0x40, 0x40 };
/* SDL's fixed frequencies: the high band 0x0074, the low band 0x3d */
const uint16_t hf = 0x0074;
const uint8_t lf = 0x3D;
uint8_t ha;
/* The low band takes the stronger of the two motors, not the large one
* alone: third-party pads (the GameSir G8 through the USB Wireless
* Adapter 2) hardly feel the high band, and some games rumble with the
* small motor only (Crash Bandicoot's spin: 0/63). */
uint16_t la;
unsigned char d[4];
if (!large && !small) {
memcpy(out, neutral, 4);
memcpy(out + 4, neutral, 4);
return;
}
large = switch_rumble_boost(large);
small = switch_rumble_boost(small);
ha = high_amp((uint16_t)(small * 257));
la = low_amp((uint16_t)((large > small ? large : small) * 257));
d[0] = (unsigned char)(hf & 0xFF);
d[1] = (unsigned char)(ha | ((hf >> 8) & 0x01));
d[2] = (unsigned char)(lf | ((la >> 8) & 0x80));
d[3] = (unsigned char)(la & 0xFF);
memcpy(out, d, 4);
memcpy(out + 4, d, 4);
}
+47
View File
@@ -0,0 +1,47 @@
/* The Nintendo Switch Pro Controller over USB (057e:2009): what the 8BitDo USB
* Wireless Adapter 2 shows in its Switch mode, and controllers wired in their
* Switch mode. Protocol as documented by dekuNukem's Nintendo Switch reverse
* engineering notes and implemented by SDL (zlib licence) and Linux's
* hid-nintendo.
*
* Report 0x30 (and 0x21, a subcommand's reply, which starts the same):
* [3] Y X B A SR SL R ZR [4] - + RS LS Home Capture [5] down up right left SR SL L ZL
* [6..8] left stick, [9..11] right stick: two 12-bit values each, y grows upwards
* Placed by position, as on a DualSense: B cross, A circle, Y square, X triangle;
* Capture is the touchpad. */
#ifndef PADBRIDGE_MAPSWITCH_H
#define PADBRIDGE_MAPSWITCH_H
#include "pad.h"
#include <stdint.h>
/* Stick calibration, learnt as the pad is used: the centre from the first
* report (sticks at rest), the reach from the furthest seen, at least
* SWITCH_STICK_REACH. */
#define SWITCH_STICK_REACH 1500
typedef struct {
int set;
int centre[4], reach[4]; /* lx ly rx ry */
} switch_sticks;
void switch_sticks_reset(switch_sticks *c);
/* Report 0x30 or 0x21 to a DualSense state. 0 if it is neither. */
int switch_parse(const unsigned char *r, int n, switch_sticks *cal, pad_state *st);
/* The 8 rumble bytes (left and right, the same) for a large and a small motor
* strength, 0..255: SDL's encoding and tables. The high band follows the small
* motor; the low band the stronger of the two (third-party pads hardly feel
* the high band). */
void switch_rumble_encode(uint8_t large, uint8_t small, unsigned char out[8]);
/* A motor strength lifted over the threshold of simple rumble motors: 0 stays
* 0, anything else goes from SWITCH_RUMBLE_FLOOR to 255. A weak request (Crash
* Bandicoot's spin asks 63) is otherwise not felt at all on a third-party pad
* through the adapter. */
#define SWITCH_RUMBLE_FLOOR 100
uint8_t switch_rumble_boost(uint8_t v);
#endif
+14
View File
@@ -78,6 +78,7 @@ int32_t scePadClose(int32_t handle);
int32_t scePadReadState(int32_t handle, ScePadData *data);
int32_t scePadVirtualDeviceAddDevice(void *param, int32_t deviceType);
int32_t scePadVirtualDeviceInsertData(int32_t handle, const void *data);
int32_t scePadVirtualDeviceGetRemoteSetting(int32_t handle, void *setting);
int32_t scePadVirtualDeviceDeleteDevice(int32_t handle);
/* From the payload SDK (ps5/kernel.h). */
@@ -564,6 +565,19 @@ void vpad_shutdown(void)
log_source_close();
}
/* padbridge: what the console asks of the slot's virtual pad (rumble, light
* bar), as scePadVirtualDeviceGetRemoteSetting returns it: close to a
* DualShock 4 output report, [0] update flag, [3] large motor, [4] small
* motor, [5..7] light bar, [56] a user is assigned (seen on PS5 13.60, as
* Control4Free found on a PS4). The buffer is cleared first. The
* call's return, or -1 if the slot has no live pad. */
int32_t vpad_remote_setting(int slot, uint8_t *buf, size_t size)
{
memset(buf, 0, size);
if (slot < 0 || slot >= HOST_MAX_PADS || g_slot[slot].st != VP_READY || g_slot[slot].sys_off) return -1;
return scePadVirtualDeviceGetRemoteSetting(g_slot[slot].handle, buf);
}
int32_t vpad_user(int slot)
{
if (slot < 0 || slot >= HOST_MAX_PADS || g_slot[slot].st != VP_READY) return -1;
+4
View File
@@ -46,6 +46,10 @@ void vpad_remove(int slot);
* caller removes it and asks for a new one when the user presses PS (Home). */
int vpad_off(int slot);
/* padbridge: what the console asks of the slot's virtual pad (rumble, light
* bar), as the system keeps it: the call's return, -1 if no live pad. */
int32_t vpad_remote_setting(int slot, uint8_t *buf, size_t size);
/* padbridge: the user the slot's live pad is given to, -1 if none (or not known). */
int32_t vpad_user(int slot);
int vpad_user_name(int32_t user, char *out, size_t size); /* 0 if unknown */
+4 -1
View File
@@ -42,7 +42,7 @@ static int pick(const iface *ifs, int n, const iface *f, int kind, usbdesc_pick
return kind;
}
int usbdesc_choose(const unsigned char *cfg, int len, uint16_t vid, usbdesc_pick *out)
int usbdesc_choose(const unsigned char *cfg, int len, uint16_t vid, uint16_t pid, usbdesc_pick *out)
{
iface ifs[16];
int n = 0, off = 0, i;
@@ -76,6 +76,9 @@ int usbdesc_choose(const unsigned char *cfg, int len, uint16_t vid, usbdesc_pick
for (i = 0; i < n; i++)
if (is_xinput(&ifs[i]) && ifs[i].ep_in) return pick(ifs, n, &ifs[i], USBPAD_XINPUT, out);
if (vid == 0x057E && pid == 0x2009)
for (i = 0; i < n; i++)
if (ifs[i].cls == 0x03 && ifs[i].ep_in) return pick(ifs, n, &ifs[i], USBPAD_SWITCH, out);
if (vid == VID_8BITDO)
for (i = 0; i < n; i++)
if (is_pad_hid(&ifs[i]) && ifs[i].ep_in) return pick(ifs, n, &ifs[i], USBPAD_8BITDO, out);
+4 -2
View File
@@ -12,6 +12,7 @@ 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) */
USBPAD_SWITCH, /* Nintendo Switch Pro Controller, 057e:2009 (mapswitch.c) */
};
typedef struct {
@@ -25,10 +26,11 @@ typedef struct {
/* 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
* 2. a Switch Pro Controller (057e:2009): its HID interface;
* 3. 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);
int usbdesc_choose(const unsigned char *cfg, int len, uint16_t vid, uint16_t pid, 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. */
+186 -9
View File
@@ -1,5 +1,6 @@
#include "usbpad.h"
#include "log.h"
#include "mapswitch.h"
#include "usbdesc.h"
#include "util.h"
@@ -50,6 +51,11 @@ struct usbpad {
unsigned char busy[N_RD + 1];
long t_alive;
int logged_first;
unsigned char rumble[2], rumble_sent[2]; /* large, small motor: wanted, last sent */
int rumble_pending;
long rumble_at; /* ms: last rumble packet sent */
switch_sticks swcal; /* Switch Pro: stick centres and reach */
unsigned char swcount; /* Switch Pro: output packet number, 0..15 */
};
static int dead_errno(int e)
@@ -58,7 +64,7 @@ static int dead_errno(int e)
}
/* The interface to read, from the full configuration descriptor. */
static int choose_interface(int fd, uint16_t vid, usbdesc_pick *p)
static int choose_interface(int fd, uint16_t vid, uint16_t pid, usbdesc_pick *p)
{
struct usb_gen_descriptor gd;
unsigned char cfg[1024];
@@ -70,7 +76,7 @@ static int choose_interface(int fd, uint16_t vid, usbdesc_pick *p)
gd.ugd_config_index = 0xFF;
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;
return usbdesc_choose(cfg, len, vid, p);
return usbdesc_choose(cfg, len, vid, pid, p);
}
/* Sends one interrupt OUT transfer and waits for it, a short while. */
@@ -168,11 +174,109 @@ static int start_read(usbpad *u, int i)
return 1;
}
/* ---- the Switch Pro Controller's setup over USB --------------------------
*
* Until the host has done this, the pad (or the USB Wireless Adapter 2 in its
* Switch mode) sends nothing but a status and enumerates again every few
* seconds. As SDL does it (BTrySetupUSB and after): the handshake 80 02, high
* speed 80 03, 80 02 again, then 80 04 "stay on USB"; then subcommands on
* report 01: the full input report 0x30, vibration on, player 1's light.
* Done once at connection, a few hundred milliseconds at most. */
/* One read on transfer 0, waiting at most `ms`. Its length, 0 if none. */
static int read_once(usbpad *u, unsigned char *out, int ms)
{
struct usb_fs_complete done;
long t0 = now_ms();
if (!u->busy[0] && start_read(u, 0) <= 0) return 0;
while (now_ms() - t0 < ms) {
memset(&done, 0, sizeof done);
if (ioctl(u->fd, USB_FS_COMPLETE, &done) != 0) {
usleep(1000);
continue;
}
if (done.ep_index > N_RD) continue;
u->busy[done.ep_index] = 0;
if (done.ep_index != 0) continue;
if (u->ep[0].status || !u->ep[0].aFrames) return 0;
memcpy(out, u->buf[0], RD_MAX);
return (int)u->lenp[0][0];
}
return 0;
}
/* Waits for a reply whose first byte is `id` and byte `at` is `sub` (sub < 0:
* any). 1 if it came within `ms`. Other reports on the way are dropped. */
static int wait_reply(usbpad *u, int id, int at, int sub, int ms)
{
unsigned char r[RD_MAX];
long t0 = now_ms();
while (now_ms() - t0 < ms) {
int n = read_once(u, r, ms - (int)(now_ms() - t0));
if (n > at && r[0] == id && (sub < 0 || r[at] == sub)) return 1;
}
return 0;
}
static int switch_proprietary(usbpad *u, int cmd, int wait)
{
unsigned char p[64];
int tries;
for (tries = 0; tries < 3; tries++) {
memset(p, 0, sizeof p);
p[0] = 0x80;
p[1] = (unsigned char)cmd;
if (!send_out(u, p, sizeof p)) continue;
if (!wait || wait_reply(u, 0x81, 1, cmd, 100)) return 1;
}
return 0;
}
static int switch_subcommand(usbpad *u, int sub, const unsigned char *args, int n)
{
unsigned char p[64];
int tries;
for (tries = 0; tries < 3; tries++) {
memset(p, 0, sizeof p);
p[0] = 0x01;
p[1] = u->swcount;
u->swcount = (unsigned char)((u->swcount + 1) & 0x0F);
switch_rumble_encode(0, 0, p + 2); /* no rumble */
p[10] = (unsigned char)sub;
if (n > 0) memcpy(p + 11, args, (size_t)n);
if (!send_out(u, p, sizeof p)) continue;
if (wait_reply(u, 0x21, 14, sub, 100)) return 1;
}
return 0;
}
static void switch_setup(usbpad *u)
{
static const unsigned char full = 0x30, on = 0x01, player1 = 0x01;
int hs, fu, mode, vib, led;
switch_sticks_reset(&u->swcal);
hs = switch_proprietary(u, 0x02, 1);
switch_proprietary(u, 0x03, 1); /* some pads do not answer: no matter */
switch_proprietary(u, 0x02, 1);
fu = switch_proprietary(u, 0x04, 0);
mode = switch_subcommand(u, 0x03, &full, 1);
vib = switch_subcommand(u, 0x48, &on, 1);
led = switch_subcommand(u, 0x30, &player1, 1);
log_line("usb: %s: Switch Pro setup: handshake %s, USB %s, full reports %s, vibration %s, light %s", u->path,
hs ? "ok" : "no answer", fu ? "ok" : "failed", mode ? "ok" : "no answer", vib ? "ok" : "no answer",
led ? "ok" : "no answer");
}
/* *not_pad: set when the device answered and is not a pad (as opposed to a
* failure on the way, which is tried again). */
static usbpad *try_node(int bus, int addr, int *not_pad)
{
static const char *kinds[] = { "", "8BitDo HID (DInput)", "XInput" };
static const char *kinds[] = { "", "8BitDo HID (DInput)", "XInput", "Switch Pro" };
struct usb_device_descriptor dd;
struct usb_device_info di;
struct usb_fs_init init;
@@ -190,7 +294,7 @@ static usbpad *try_node(int bus, int addr, int *not_pad)
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;
u->kind = choose_interface(u->fd, UGETW(dd.idVendor), &pick);
u->kind = choose_interface(u->fd, UGETW(dd.idVendor), UGETW(dd.idProduct), &pick);
if (u->kind == USBPAD_NONE) {
char prod[48] = "";
read_string(u->fd, dd.iProduct, prod, sizeof prod);
@@ -201,7 +305,7 @@ static usbpad *try_node(int bus, int addr, int *not_pad)
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) {
if ((u->kind == USBPAD_XINPUT || u->kind == USBPAD_SWITCH) && 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;
@@ -262,7 +366,7 @@ static usbpad *try_node(int bus, int addr, int *not_pad)
}
u->t_alive = now_ms();
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);
UGETW(dd.idVendor), UGETW(dd.idProduct), u->name, (size_t)u->kind < sizeof kinds / sizeof kinds[0] ? kinds[u->kind] : "?", pick.iface, u->ep_in, u->mps);
/* A keyboard or mouse beside the pad (the USB Wireless Adapter 2's macros
* in XInput mode): the console's own driver lets go of it, so the console
* does not list a keyboard and a mouse among its accessories. */
@@ -272,6 +376,7 @@ static usbpad *try_node(int bus, int addr, int *not_pad)
log_line("usb: %s: interface %d (keyboard/mouse) hidden from the console: %s", u->path, i,
ioctl(u->fd, USB_IFACE_DRIVER_DETACH, &n) == 0 ? "ok" : "failed");
}
if (u->kind == USBPAD_SWITCH) switch_setup(u);
if (u->kind == USBPAD_XINPUT) {
xinput_windows_hello(u);
/* The LED of player 1, as a PC sets it: some XInput pads report only after it. */
@@ -316,7 +421,7 @@ usbpad *usbpad_open(void)
/* Logged once per device: what is plugged in and not read, to
* tell why a controller does nothing (another mode, say). */
if (not_pad && strcmp(g_logged[bus][addr], g_reject) != 0) {
log_line("usb: %s %s: not a controller padbridge reads", path, g_reject);
log_line("usb: %s %s: not read by padbridge", path, g_reject);
snprintf(g_logged[bus][addr], sizeof g_logged[bus][addr], "%s", g_reject);
}
g_not_pad[bus][addr].set = (unsigned char)not_pad;
@@ -334,7 +439,77 @@ const char *usbpad_name(const usbpad *u)
const char *usbpad_mode(const usbpad *u)
{
return u && u->kind == USBPAD_XINPUT ? "XInput" : "DInput";
if (!u) return "DInput";
return u->kind == USBPAD_XINPUT ? "XInput" : u->kind == USBPAD_SWITCH ? "Switch" : "DInput";
}
/* Starts an OUT transfer without waiting: its completion comes through
* usbpad_poll. 0 if it could not start. */
static int start_out(usbpad *u, const unsigned char *data, int n)
{
struct usb_fs_start st;
if (!u->ep_out || u->busy[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;
return 1;
}
int usbpad_can_rumble(const usbpad *u)
{
return u && (u->kind == USBPAD_XINPUT || u->kind == USBPAD_SWITCH) && u->ep_out;
}
void usbpad_rumble(usbpad *u, uint8_t large, uint8_t small)
{
if (!usbpad_can_rumble(u)) return;
u->rumble[0] = large;
u->rumble[1] = small;
u->rumble_pending = large != u->rumble_sent[0] || small != u->rumble_sent[1];
}
/* Rumble, sent when the OUT endpoint is free; a newer value waiting replaces
* an older one.
* XInput: 00 08 00 <large> <small> 00 00 00
* Switch Pro: report 0x10, packet number, 8 rumble bytes (mapswitch.c),
* sent again every 50 ms while on, as SDL does: the pad stops
* rumbling when it is not refreshed. */
#define T_SWITCH_RUMBLE 50
static void rumble_flush(usbpad *u, long now)
{
unsigned char r[64];
int n;
if (u->kind == USBPAD_SWITCH && (u->rumble_sent[0] || u->rumble_sent[1]) && now - u->rumble_at >= T_SWITCH_RUMBLE)
u->rumble_pending = 1;
if (!u->rumble_pending || u->busy[EP_OUT]) return;
memset(r, 0, sizeof r);
if (u->kind == USBPAD_SWITCH) {
r[0] = 0x10;
r[1] = u->swcount;
switch_rumble_encode(u->rumble[0], u->rumble[1], r + 2);
n = 64;
} else {
r[1] = 0x08;
r[3] = u->rumble[0];
r[4] = u->rumble[1];
n = 8;
}
if (start_out(u, r, n)) {
if (u->kind == USBPAD_SWITCH) u->swcount = (unsigned char)((u->swcount + 1) & 0x0F);
u->rumble_at = now;
u->rumble_sent[0] = u->rumble[0];
u->rumble_sent[1] = u->rumble[1];
u->rumble_pending = 0;
}
}
int usbpad_poll(usbpad *u, pad_state *st)
@@ -364,7 +539,8 @@ int usbpad_poll(usbpad *u, pad_state *st)
if (u->ep[i].status) continue; /* cancelled or stalled: read again */
if (!u->ep[i].aFrames) continue;
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)) {
: u->kind == USBPAD_SWITCH ? switch_parse(u->buf[i], (int)u->lenp[i][0], &u->swcal, 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");
@@ -372,6 +548,7 @@ int usbpad_poll(usbpad *u, pad_state *st)
got = 1;
}
}
rumble_flush(u, now);
/* A re-enumeration does not always fail the pending reads at once.
* (USB_GET_DEVICEINFO fails on the 8BitDo dongle, so it is not used.) */
if (now - u->t_alive >= T_ALIVE) {
+8
View File
@@ -15,6 +15,8 @@
#include "pad.h"
#include <stdint.h>
typedef struct usbpad usbpad;
/* Looks for a supported pad not open already. NULL if there is none right
@@ -27,6 +29,12 @@ usbpad *usbpad_open(void);
int usbpad_poll(usbpad *u, pad_state *st);
void usbpad_close(usbpad *u);
/* Rumble, 0..255 per motor (large: strong and slow, small: fast). Sent on the
* next usbpad_poll; only XInput pads with an OUT endpoint for now (the
* 8BitDo USB Wireless Adapter 2 passes it on to PlayStation controllers). */
int usbpad_can_rumble(const usbpad *u);
void usbpad_rumble(usbpad *u, uint8_t large, uint8_t small);
const char *usbpad_name(const usbpad *u); /* the product, as the device names itself */
const char *usbpad_mode(const usbpad *u); /* "DInput" or "XInput" */
+1 -1
View File
@@ -1,7 +1,7 @@
#ifndef PADBRIDGE_VERSION_H
#define PADBRIDGE_VERSION_H
#define PADBRIDGE_VERSION "1.2.0"
#define PADBRIDGE_VERSION "1.3.0"
#define PADBRIDGE_AUTHOR "Sub" /* every notification starts "padbridge by Sub" */
#endif
+58
View File
@@ -0,0 +1,58 @@
/* View + Menu as the touchpad, for pads without one (XInput). Runs on the PC. */
#include "combo.h"
#include <stdio.h>
static int fails;
#define CHECK(cond, ...) do { if (!(cond)) { printf("FAIL " __VA_ARGS__); printf("\n"); fails++; } } while (0)
#define V PAD_CREATE
#define M PAD_OPTIONS
#define T PAD_TOUCHPAD
int main(void)
{
combo c;
uint32_t o;
long t;
/* both at once: the touchpad, neither of them */
combo_reset(&c);
CHECK(combo_filter(&c, V | M | PAD_CROSS, 0) == (T | PAD_CROSS), "both at once");
CHECK(combo_filter(&c, V | M, 30) == T, "both held");
CHECK(combo_filter(&c, V, 40) == T, "one let go: still the touchpad until both are");
CHECK(combo_filter(&c, 0, 50) == 0, "both let go");
CHECK(combo_filter(&c, V, 200) == 0, "View again: held back");
/* View first, Menu within the window: the touchpad, no View on the way */
combo_reset(&c);
CHECK(combo_filter(&c, V, 0) == 0, "View alone at first: held back");
CHECK(combo_filter(&c, V, 30) == 0, "View alone, 30 ms: held back");
CHECK(combo_filter(&c, V | M, 45) == T, "Menu joins: touchpad");
CHECK(combo_filter(&c, 0, 120) == 0, "released");
/* View held alone past the window: View, as usual */
combo_reset(&c);
combo_filter(&c, V, 0);
CHECK(combo_filter(&c, V, COMBO_WINDOW_MS) == V, "View held past the window");
CHECK(combo_filter(&c, V, 500) == V, "View still held");
CHECK(combo_filter(&c, V | M, 520) == (V | M), "Menu late: both as they are");
CHECK(combo_filter(&c, 0, 600) == 0, "released");
/* a quick tap of Menu, shorter than the window, is not lost */
combo_reset(&c);
CHECK(combo_filter(&c, M, 1000) == 0, "tap: held back");
o = combo_filter(&c, 0, 1030);
CHECK(o == M, "tap released early: sent now (%#x)", (unsigned)o);
for (t = 1031; t < 1030 + COMBO_TAP_MS; t += 10)
if (combo_filter(&c, 0, t) != M) { CHECK(0, "tap not held for its length at %ld", t); break; }
CHECK(combo_filter(&c, 0, 1030 + COMBO_TAP_MS) == 0, "tap over");
/* other buttons go through untouched */
combo_reset(&c);
CHECK(combo_filter(&c, PAD_CROSS | PAD_L1, 0) == (PAD_CROSS | PAD_L1), "others");
printf("%s (%d failure(s))\n", fails ? "FAILED" : "all passed", fails);
return fails != 0;
}
+157
View File
@@ -0,0 +1,157 @@
/* The Switch Pro Controller's report 0x30 and its rumble. Runs on the PC. */
#include "mapswitch.h"
#include "usbdesc.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)
/* A report 0x30 with buttons (right, shared, left bytes) and raw 12-bit sticks. */
static void report(unsigned char r[64], int right, int shared, int left, int lx, int ly, int rx, int ry)
{
memset(r, 0, 64);
r[0] = 0x30;
r[3] = (unsigned char)right;
r[4] = (unsigned char)shared;
r[5] = (unsigned char)left;
r[6] = (unsigned char)(lx & 0xFF);
r[7] = (unsigned char)(((lx >> 8) & 0x0F) | ((ly & 0x0F) << 4));
r[8] = (unsigned char)(ly >> 4);
r[9] = (unsigned char)(rx & 0xFF);
r[10] = (unsigned char)(((rx >> 8) & 0x0F) | ((ry & 0x0F) << 4));
r[11] = (unsigned char)(ry >> 4);
}
static uint32_t buttons(int right, int shared, int left)
{
unsigned char r[64];
switch_sticks cal;
pad_state st;
switch_sticks_reset(&cal);
report(r, right, shared, left, 2048, 2048, 2048, 2048);
if (!switch_parse(r, 64, &cal, &st)) return 0xFFFFFFFFu;
return st.buttons;
}
static void test_descriptor(void)
{
static const unsigned char pro[] = {
9, 0x02, 41, 0, 1, 1, 0, 0xC0, 0xFA,
9, 0x04, 0, 0, 2, 0x03, 0, 0, 0,
9, 0x21, 0x11, 0x01, 0, 1, 0x22, 0xCB, 0,
7, 0x05, 0x81, 0x03, 64, 0, 4,
7, 0x05, 0x02, 0x03, 64, 0, 8,
};
usbdesc_pick p;
CHECK(usbdesc_choose(pro, sizeof pro, 0x057E, 0x2009, &p) == USBPAD_SWITCH, "Pro Controller not chosen");
CHECK(p.ep_in == 0x81 && p.ep_out == 0x02 && p.mps_out == 64, "Pro endpoints %#x %#x", p.ep_in, p.ep_out);
CHECK(usbdesc_choose(pro, sizeof pro, 0x057E, 0x2017, &p) == USBPAD_NONE, "another Nintendo pid taken");
}
static void test_buttons(void)
{
/* by position, as on a DualSense: B bottom cross, A right circle, Y left square, X top triangle */
CHECK(buttons(0x04, 0, 0) == PAD_CROSS, "B");
CHECK(buttons(0x08, 0, 0) == PAD_CIRCLE, "A");
CHECK(buttons(0x01, 0, 0) == PAD_SQUARE, "Y");
CHECK(buttons(0x02, 0, 0) == PAD_TRIANGLE, "X");
CHECK(buttons(0x40, 0, 0) == PAD_R1, "R");
CHECK(buttons(0x80, 0, 0) == (PAD_R2), "ZR");
CHECK(buttons(0, 0, 0x40) == PAD_L1, "L");
CHECK(buttons(0, 0, 0x80) == PAD_L2, "ZL");
CHECK(buttons(0, 0x01, 0) == PAD_CREATE, "Minus");
CHECK(buttons(0, 0x02, 0) == PAD_OPTIONS, "Plus");
CHECK(buttons(0, 0x04, 0) == PAD_R3, "R stick");
CHECK(buttons(0, 0x08, 0) == PAD_L3, "L stick");
CHECK(buttons(0, 0x10, 0) == PAD_PS, "Home");
CHECK(buttons(0, 0x20, 0) == PAD_TOUCHPAD, "Capture");
CHECK(buttons(0, 0, 0x01) == PAD_DOWN, "down");
CHECK(buttons(0, 0, 0x02) == PAD_UP, "up");
CHECK(buttons(0, 0, 0x04) == PAD_RIGHT, "right");
CHECK(buttons(0, 0, 0x08) == PAD_LEFT, "left");
CHECK(buttons(0x30, 0, 0x30) == 0, "SL/SR (Joy-Con rail) ignored");
}
static void test_triggers_and_sticks(void)
{
unsigned char r[64];
switch_sticks cal;
pad_state st;
switch_sticks_reset(&cal);
report(r, 0x80, 0, 0x80, 2048, 2048, 2048, 2048);
switch_parse(r, 64, &cal, &st);
CHECK(st.l2 == 255 && st.r2 == 255, "ZL/ZR analog %u %u", st.l2, st.r2);
/* the first report sets the centre: here off 2048, as a real pad is */
switch_sticks_reset(&cal);
report(r, 0, 0, 0, 1990, 2110, 2010, 2070);
switch_parse(r, 64, &cal, &st);
CHECK(st.lx == 128 && st.ly == 128 && st.rx == 128 && st.ry == 128, "centre %u %u %u %u", st.lx, st.ly, st.rx, st.ry);
/* full left and up (y grows upwards on a Switch pad) */
report(r, 0, 0, 0, 1990 - 1500, 2110 + 1500, 2010 + 1500, 2070 - 1500);
switch_parse(r, 64, &cal, &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);
/* half way stays about half way */
report(r, 0, 0, 0, 1990 + 650, 2110, 2010, 2070);
switch_parse(r, 64, &cal, &st);
CHECK(st.lx > 180 && st.lx < 210, "half right %u", st.lx);
/* a reply to a subcommand (0x21) carries the same controller state */
report(r, 0x04, 0, 0, 1990, 2110, 2010, 2070);
r[0] = 0x21;
CHECK(switch_parse(r, 64, &cal, &st) && st.buttons == PAD_CROSS, "0x21 state");
r[0] = 0x81;
CHECK(!switch_parse(r, 64, &cal, &st), "0x81 taken as state");
CHECK(!switch_parse(r, 8, &cal, &st), "a short report taken");
}
static void test_rumble(void)
{
unsigned char d[8];
static const unsigned char neutral[8] = { 0x00, 0x01, 0x40, 0x40, 0x00, 0x01, 0x40, 0x40 };
switch_rumble_encode(0, 0, d);
CHECK(!memcmp(d, neutral, 8), "no rumble is the neutral value");
switch_rumble_encode(255, 255, d);
CHECK(!memcmp(d, d + 4, 4), "both sides the same");
CHECK(d[0] == 0x74 && (d[1] & 0xFE) == 0xC8, "full high band: %02x %02x", d[0], d[1]);
CHECK((d[2] & 0x7F) == 0x3D && d[3] == 0x72, "full low band: %02x %02x", d[2], d[3]);
/* the small motor alone (Crash's spin: 0/63) must reach the low band too:
* third-party pads (the GameSir G8 through the adapter) hardly feel the
* high band */
switch_rumble_encode(0, 63, d);
CHECK((d[1] & 0xFE) != 0, "small motor: high band amplitude (%02x)", d[1]);
CHECK(d[3] != 0x40 || (d[2] & 0x80), "small motor alone: low band silent (%02x %02x)", d[2], d[3]);
/* weak requests are lifted over the motors' threshold: 63 (25%) ends up
* well past the low band's quarter */
switch_rumble_encode(0, 63, d);
CHECK(((d[2] & 0x80) << 1 | d[3]) >= 0x0060, "a weak request stays below the threshold (%02x %02x)", d[2], d[3]);
CHECK(switch_rumble_boost(0) == 0 && switch_rumble_boost(255) == 255, "boost ends");
CHECK(switch_rumble_boost(1) >= SWITCH_RUMBLE_FLOOR, "boost floor");
switch_rumble_encode(128, 0, d);
CHECK(memcmp(d, neutral, 4) != 0, "the large motor alone rumbles");
CHECK((d[1] & 0xFE) == 0x00, "large motor only: no high band amplitude (%02x)", d[1]);
}
int main(void)
{
test_descriptor();
test_buttons();
test_triggers_and_sticks();
test_rumble();
printf("%s (%d failure(s))\n", fails ? "FAILED" : "all passed", fails);
return fails != 0;
}
+8 -8
View File
@@ -41,23 +41,23 @@ 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(usbdesc_choose(desc_310a, sizeof desc_310a, VID_8BITDO, 0, &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_310a, sizeof desc_310a, 0x045E, 0, &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(usbdesc_choose(desc_hid, sizeof desc_hid, VID_8BITDO, 0, &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_hid, sizeof desc_hid, 0x1234, 0, &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");
CHECK(usbdesc_choose(desc_kbd, sizeof desc_kbd, VID_8BITDO, 0, &p) == USBPAD_NONE, "boot keyboard taken");
CHECK(usbdesc_choose(desc_310a, 20, VID_8BITDO, 0, &p) == USBPAD_NONE, "truncated descriptor taken");
/* the boot keyboard beside the pad is to be hidden from the console */
usbdesc_choose(desc_310a, sizeof desc_310a, VID_8BITDO, &p);
usbdesc_choose(desc_310a, sizeof desc_310a, VID_8BITDO, 0, &p);
CHECK(p.hide == (1u << 1), "310a: hide %#x, want interface 1", (unsigned)p.hide);
usbdesc_choose(desc_hid, sizeof desc_hid, VID_8BITDO, &p);
usbdesc_choose(desc_hid, sizeof desc_hid, VID_8BITDO, 0, &p);
CHECK(p.hide == 0, "301c: hide %#x", (unsigned)p.hide);
}