3 Commits
Author SHA1 Message Date
Sub 3d97019644 padbridge 1.3.1: rest mode no longer stops it (#7)
padbridge stopped before rest mode (as AnyPad PS5 did). An autoload list
only runs at a cold start, so after waking the console had no padbridge,
and the Ultimate 2C's dongle in XInput mode showed up as a keyboard.

Now, in rest mode, padbridge lets the controllers go (rumble off,
virtual pads removed, USB closed), waits for the console to work again
(or for its stop flag), then closes the kernel log, which is opened anew
when a pad is added, and looks for controllers again.

Tested on the PS5: rest mode for about three minutes: "rest mode:
controllers let go", "awake: looking for controllers again", and the 2C
connected again on its own. A shorter rest, where the console froze
padbridge before it saw it, also came back.
2026-10-23 13:38:21 -03:00
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
Sub f29a1de877 padbridge 1.2.0: pad2c renamed, README rewritten (#5)
* padbridge 1.2.0: pad2c renamed, README rewritten

pad2c is no longer about one controller: it reads the Ultimate 2C dongle,
the 8BitDo USB Wireless Adapter 2 with whatever is paired with it, and
wired XInput pads. Hence padbridge.

- padbridge-<version>.elf, padbridge-probe.elf; notifications and the
  settings page read "padbridge by Sub"; files in /data/padbridge.
- From pad2c: on the first start a running pad2c is asked to stop (its
  own stop flag), and /data/pad2c moves to /data/padbridge with the
  remembered users and settings; the log is renamed.
- The log names, once per device, every USB device plugged in that
  padbridge does not read (a controller in another mode: the GameSir G8
  on its cable in Switch mode is 057e:2009 "Pro Controller").
- README rewritten: compatibility tables (adapter, dongle, cable, ports),
  the settings page with a screenshot, the adapter's modes, upgrading
  from pad2c. The old 1.0 photos are gone.
- dist/ ignored as a whole (two .sha256 files had slipped in).

Tested on the PS5: the move from a running pad2c 1.1.0; the Ultimate 2C
on a USB cable (2dc8:310a) in the USB-A and USB-C ports; the G8 on its
cable in Switch mode logged once as not read.

* CHANGELOG: the log of devices not read; the 2C on a cable

* README: no mention of the G8 on a cable

* README: no section on cables and ports
2026-10-22 18:25:37 -03:00
34 changed files with 1198 additions and 243 deletions

No files matched your search

+4 -4
View File
@@ -1,6 +1,6 @@
---
name: Controller report
about: Tried pad2c with a controller (working or not), or want support for a new one
about: Tried padbridge with a controller (working or not), or want support for a new one
labels: controller
---
@@ -10,13 +10,13 @@ labels: controller
**What happened** (works / partly / not detected):
**pad2c log** (`/data/pad2c/pad2c.log`, the lines from the last start):
**padbridge log** (`/data/padbridge/padbridge.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.
**For a controller that is not supported yet:** send `padbridge-probe.elf` with the controller plugged in, press every
button one at a time while it runs, and paste `/data/padbridge/probe.log` here.
```
```
+1 -2
View File
@@ -1,4 +1,3 @@
build/
dist/*.elf
!dist/.keep
dist/
release/
+35
View File
@@ -1,5 +1,40 @@
# Changelog
## 1.3.1
- **Rest mode no longer stops padbridge.** It used to stop before rest mode (as AnyPad PS5 did), and since an
autoload list only runs at a cold start, after waking the console had no padbridge: a 2C dongle in XInput mode then
showed up as a keyboard. Now padbridge lets the controllers go (rumble off, virtual pads removed, USB closed),
waits, and looks for them again when the console wakes, with the kernel log opened anew.
## 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
start it asks a running pad2c to stop and moves `/data/pad2c` there, remembered users and settings included.
- The log names, once per device, every USB device plugged in that padbridge does not read: a controller in another
mode shows up there (the GameSir G8 on its cable in Switch mode is `057e:2009` "Pro Controller").
- Tested: the Ultimate 2C on a USB cable (XInput, `2dc8:310a`), in the USB-A and USB-C ports.
- README rewritten around what is supported and tested: the Ultimate 2C dongle, the USB Wireless Adapter 2 and the
controllers tested through it, controllers on a USB cable, and the settings page (with a screenshot).
## 1.1.0
- The **8BitDo USB Wireless Adapter 2** (#1): any controller it pairs with (tested: DualSense, Ultimate 2C, GameSir G8
Plus Galileo) becomes a virtual DualSense. Plugged in, the adapter tries several identities and keeps the one whose
+10 -6
View File
@@ -1,11 +1,11 @@
# pad2c: the 8BitDo dongle as a second DualSense on a jailbroken PS5.
# padbridge: the 8BitDo dongle as a second DualSense on a jailbroken PS5.
#
# make test the mapping test, on this machine
# make the payloads into dist/ (needs PS5_PAYLOAD_SDK)
VERSION := $(shell sed -n 's/^\#define PAD2C_VERSION "\(.*\)"/\1/p' src/version.h)
VERSION := $(shell sed -n 's/^\#define PADBRIDGE_VERSION "\(.*\)"/\1/p' src/version.h)
all: dist/pad2c-$(VERSION).elf dist/pad2c-probe.elf
all: dist/padbridge-$(VERSION).elf dist/padbridge-probe.elf
test:
@mkdir -p build
@@ -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,13 +29,13 @@ 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/pad2c-$(VERSION).elf: $(SRCS) $(wildcard src/*.h)
dist/padbridge-$(VERSION).elf: $(SRCS) $(wildcard src/*.h)
@mkdir -p dist
$(CC) $(CFLAGS) -o $@ $(SRCS) $(LDLIBS)
dist/pad2c-probe.elf: src/probe.c src/usbdesc.c src/usbdesc.h
dist/padbridge-probe.elf: src/probe.c src/usbdesc.c src/usbdesc.h
@mkdir -p dist
$(CC) $(CFLAGS) -o $@ src/probe.c src/usbdesc.c
+197 -133
View File
@@ -1,166 +1,225 @@
# pad2c
# padbridge
**Use an [8BitDo Ultimate 2C Wireless Controller](https://www.8bitdo.com/ultimate-2c-wireless-controller/), or
almost any Bluetooth controller through the [8BitDo USB Wireless Adapter 2](https://www.8bitdo.com/usb-wireless-adapter-2/),
on a jailbroken PS5, next to your DualSense.**
**Use controllers the PS5 does not accept, on a jailbroken PS5, as extra DualSense controllers.**
pad2c is a payload for a jailbroken PS5. It reads the 8BitDo 2.4G dongle or the 8BitDo USB Wireless Adapter 2
plugged into the console's USB port and shows each controller to the system and to games as a **virtual
DualSense**, up to four at once. The console's Bluetooth is not involved, so it never touches your DualSense's
pairing. A settings page, opened from a phone or a computer, shows the controllers live.
padbridge is a payload for a jailbroken PS5. It reads USB controllers and receivers plugged into the console and
shows each controller to the system and to games as a **virtual DualSense**, up to four at once, next to your own
DualSense:
## Screenshots
- the **8BitDo Ultimate 2C Wireless** through its 2.4G dongle;
- almost **any Bluetooth controller** (DualSense, DualShock 4, Xbox, Switch Pro, 8BitDo, GameSir...) through the
**8BitDo USB Wireless Adapter 2**;
- **wired XInput (Xbox 360) controllers** on a USB cable.
Switching the 8BitDo on and off, with pad2c running on the console:
The console's own Bluetooth is never touched, so your DualSense stays paired. A **settings page**, opened from a
phone or a computer, shows each controller live and lets you fix controllers with the Nintendo button layout.
Up to 1.1.0 the project was called **pad2c**; see [Upgrading from pad2c](#upgrading-from-pad2c).
<p align="center">
<img src="docs/images/connected.jpg" width="45%" alt="The PS5 shows 'pad2c: 8BitDo connected' and asks who is using the controller">
&nbsp;
<img src="docs/images/disconnected.jpg" width="45%" alt="The PS5 shows 'pad2c: 8BitDo disconnected'">
<img src="docs/images/settings-page.png" width="70%" alt="The padbridge settings page: the Ultimate 2C waiting on its dongle, and a controller connected through the USB Wireless Adapter 2 with the Nintendo layout on">
</p>
<p align="center"><em>Left: switched on; the console asks who is using the controller. Right: switched off.</em></p>
<p align="center"><em>The settings page: the Ultimate 2C on its dongle, and a GameSir G8 through the USB Wireless
Adapter 2 with the Nintendo layout on.</em></p>
## Which controller
## Compatibility
| Controller | Supported |
Everything below was tested on a **PS5 Slim (CFI-2114), firmware 13.60**.
### Through the 8BitDo USB Wireless Adapter 2
[8BitDo USB Wireless Adapter 2](https://www.8bitdo.com/usb-wireless-adapter-2/), firmware 1.10. Plug it in, pair a
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; 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 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 |
|---|---|
| [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 |
| [8BitDo USB Wireless Adapter 2](https://www.8bitdo.com/usb-wireless-adapter-2/), with any controller it pairs with | ✅ tested with a DualSense, the Ultimate 2C (Switch mode) and a GameSir G8 Plus Galileo (Switch mode, with "Nintendo layout") |
| Wired XInput (Xbox 360) controllers on a USB cable | ✅ tested with a GameSir G8 Plus Galileo |
| Other 8BitDo 2.4G dongles | untested: run `pad2c-probe.elf` and open an issue with its output |
| 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 |
## Why this controller, and why the dongle
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 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 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.
**The dongle instead of Bluetooth**, on purpose:
### Vibration
- **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.
**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:
## 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 the console's Bluetooth at all.
**Can I use my DualSense, an Xbox or a Switch Pro controller through the adapter?** Yes: the USB Wireless Adapter 2
pairs with them over its own Bluetooth, and pad2c reads the adapter. 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.
**A/B and X/Y are swapped on my controller.** Some controllers in Switch mode send the Nintendo layout (A on the
right). Tick **Nintendo layout** for it on the settings page.
**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
| | |
| Controller | Rumbles |
|---|---|
| Tested on | PS5 Slim (CFI-2114), firmware **13.60**, elfldr 0.26, kstuff-lite 1.11, Payload Manager 0.5.2 |
| Controllers | 8BitDo Ultimate 2C Wireless, 2.4G dongle, DInput (`2dc8:301c`) and XInput (`2dc8:310a`) modes; 8BitDo USB Wireless Adapter 2, firmware 1.10, XInput (`2dc8:3106`) and DInput (`2dc8:3105`) 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, up to four controllers at once |
| Not yet | vibration, light bar, motion sensors, audio |
| 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 |
Other firmwares and console models are untested.
**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.
The controller has two modes on its dongle, both work. It keeps the one last chosen:
### Not yet
- **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.
### The 8BitDo USB Wireless Adapter 2
Plug it in and switch the controller on: nothing to set. The adapter picks its mode from the host it sees; pad2c
answers it as Windows does, so it settles in **XInput** by itself, and pad2c hides the keyboard and mouse the adapter
adds in that mode, so the console lists only the controller.
**DInput** works too and also passes a DualSense's touchpad click: with the controller connected, hold **View
(Create on a DualSense) + Left** for 3 seconds (the adapter's light blinks twice). To let the adapter choose by itself
again, hold its button until its light goes off, then pair the controller again. Its DualShock 4 mode does not work
(it stops sending after a moment); pad2c's answer keeps it from ending there.
Notes:
- The 8BitDo has no touchpad: its extra **L4 / R4** buttons press the touchpad.
- "Turn off" in the console's menu disconnects the controller from the console, as with a DualSense, and Home
connects it again. The 8BitDo itself stays on until it goes to sleep by itself (the dongle's commands to switch
it off are not known).
- Vibration: PS5 games send DualSense haptics as audio to the controller, and a virtual DualSense has no known way
to receive them yet.
## Button map
| 8BitDo | DualSense |
|---|---|
| A / B / X / Y | ✕ / ○ / □ / △ (by position) |
| LB / RB / LT / RT | L1 / R1 / L2 / R2 (analog) |
| View / Menu | Create / Options |
| Home | PS |
| L3 / R3 | L3 / R3 |
| L4 / R4 | touchpad click |
Vibration in PS5 games, light bar, motion sensors, audio.
## Install
1. Plug the dongle (or the USB Wireless Adapter 2) into a USB port of the PS5 and switch the controller on.
2. Send `pad2c-<version>.elf` to your payload loader (elfldr listens on port 9021), or add it to Payload Manager
and to its autoload list, so that it starts with the console.
3. A notification says it is running, with the address of the settings page. Press **Home** on the controller: the
console asks who is using it.
For a second player, pick a **second user** (a game takes one controller per user). pad2c remembers the choice
and gives the controller back to that user next time, as a DualSense does.
1. Plug the dongle, the adapter or the controller's cable into a USB port of the PS5.
2. Send `padbridge-<version>.elf` to your payload loader (elfldr listens on port 9021), or add it to Payload
Manager and to its autoload list, so that it starts with the console.
3. A notification says it is running, with the address of the settings page.
4. Switch the controller on and press **Home** (or PS): the console asks who is using it. For a second player, pick a
**second user** (a game takes one controller per user). padbridge remembers the choice per controller and gives
it back to that user next time, as a DualSense does.
Files, all in `/data/pad2c/`:
| File | |
|---|---|
| `pad2c.log` | the log |
| `stop` | create it to stop pad2c (it also stops by itself before rest mode) |
| `owner`, `owner.1`... | the user the controller in each slot was last given to (delete to forget) |
| `config` | the settings page's choices (`swap <controller name>` for Nintendo layout) |
| `bind_user` | optional: `main`, `other` or a hexadecimal user id, to give the controller to that user at once |
It needs a console that already runs a payload loader; padbridge does not jailbreak anything itself.
## Settings page
Open **http://&lt;console IP&gt;:8090** in a browser on the same network (the address is in the notification when
pad2c starts). For each controller: its name, mode, slot and user, the **Nintendo layout** switch (A/B and X/Y
swapped, applied at once and kept), and its buttons, sticks and triggers live, to check a mapping. The page is open
to the local network, without a password.
Open **http://&lt;console IP&gt;:8090** in a browser on the same network (phone, computer, the console's own); the
address is in the notification when padbridge starts. For each controller:
- its name, mode (DInput or XInput), slot and the user it is given to;
- **Nintendo layout**: A/B and X/Y swapped, applied at once and kept, for controllers that send the Nintendo layout
(A on the right), such as the GameSir G8 in Switch mode;
- its buttons, sticks and triggers, live, to check a mapping.
The page is open to the local network, without a password. Through the adapter, a setting belongs to the adapter:
it holds for whatever controller is paired with it.
## The 8BitDo USB Wireless Adapter 2
The adapter shows a different USB identity for each mode and picks its mode from the host it sees. padbridge answers
it as Windows does, so it settles in **XInput** by itself, and hides the keyboard and mouse the adapter adds in that
mode, so the console's Accessories list shows only the controller.
| Mode | How | Notes |
|---|---|---|
| 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:
- **DInput** (the default): hold **B + Home** until it switches off, then **B + Home** again until it switches on.
- **XInput**: the same with **X + Home**.
## Button map
| Controller | DualSense |
|---|---|
| A / B / X / Y (Xbox layout) | ✕ / ○ / □ / △ (by position) |
| LB / RB / LT / RT | L1 / R1 / L2 / R2 (analog) |
| View / Menu (Back / Start, - / +) | Create / Options |
| 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
All in `/data/padbridge/` on the console:
| File | |
|---|---|
| `padbridge.log` | the log |
| `stop` | create it to stop padbridge (in rest mode it lets the controllers go and waits; it picks them up again when the console wakes) |
| `owner`, `owner.1`... | the user the controller in each slot was last given to (delete to forget) |
| `config` | the settings page's choices (`swap <controller name>` for Nintendo layout) |
| `bind_user` | optional: `main`, `other` or a hexadecimal user id, to give controllers to that user at once |
## Upgrading from pad2c
padbridge 1.2.0 is pad2c renamed. On its first start it asks a running pad2c to stop and moves `/data/pad2c` to
`/data/padbridge`, remembered users and settings included. Replace `pad2c-<version>.elf` with
`padbridge-<version>.elf` in Payload Manager's autoload list.
## FAQ
**Can I use a non-Sony controller on a PS5?** Not on a stock PS5: it only accepts its own and a few licensed
controllers. On a jailbroken PS5, padbridge makes them appear as DualSense controllers.
**Does it work in games, or only in the menus?** In games too. Give each controller its own console user (the console
asks when you press Home) and it is player 2, 3 or 4, like another DualSense.
**Do I lose my DualSense?** No. Both work at the same time; padbridge does not use the console's Bluetooth. (Pairing
the DualSense itself with the USB Wireless Adapter 2 is your choice; see above.)
**A/B and X/Y are swapped on my controller.** Tick **Nintendo layout** for it on the settings page.
**My controller does nothing.** `/data/padbridge/padbridge.log` lists every USB device plugged in that padbridge does
not read, with its id and name: a controller in another mode (Switch, DS4) shows up there.
**My controller is not listed.** Run `padbridge-probe.elf` with it plugged in and open an issue with
`/data/padbridge/probe.log` (the issue template says what to include).
## How it works
- `src/usbpad.c` opens each pad's `/dev/ugenB.A` node and reads its interrupt IN endpoint, up to four pads, each its
own virtual DualSense. The console's own HID driver holds the interface but ignores the device. New pads are
looked for every 100 ms. The internal Bluetooth chip (`ugen0.2`) is never touched.
- The USB Wireless Adapter 2 shows several identities when plugged in (8BitDo XInput, Xbox 360, DualShock 4) and
keeps the one whose host answers like Windows or a Switch. pad2c sends XInput pads what Windows sends first (the
Microsoft OS string and the XUSB request, then the LED), and detaches the console's driver from a boot keyboard or
mouse beside the pad.
- 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/usbpad.c` opens each controller's `/dev/ugenB.A` node and reads its interrupt IN endpoint, up to four, each
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
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.
- When a controller sleeps or switches off, the dongle or adapter enumerates again ("IDLE") 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.
- `src/web.c` serves the settings page without blocking the pads; `src/config.c` keeps its choices.
- `src/probe.c` is a read-only diagnostic payload: it dumps the USB descriptors and the reports of whatever is
plugged in. Useful to add support for another USB controller.
- `src/web.c` serves the settings page without ever blocking the controllers; `src/config.c` keeps its choices.
- `src/probe.c` is a read-only diagnostic payload: it dumps the USB descriptors, names and reports of whatever is
plugged in. Useful to add support for another controller.
## Build
@@ -170,7 +229,7 @@ Needs [ps5-payload-sdk](https://github.com/ps5-payload-dev/sdk) and clang, lld a
```
. ~/.ps5sdk
make test # button maps, interface choice, settings, status JSON, on the PC
make # dist/pad2c-<version>.elf and dist/pad2c-probe.elf
make # dist/padbridge-<version>.elf and dist/padbridge-probe.elf
```
## Credits
@@ -178,10 +237,15 @@ make # dist/pad2c-<version>.elf and dist/pad2c-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
GPL-3.0-or-later. See [LICENSE](LICENSE).
No Sony code, keys or firmware are part of this project. pad2c does not jailbreak anything: it needs a console
No Sony code, keys or firmware are part of this project. padbridge does not jailbreak anything: it needs a console
that already runs a payload loader.
-1
View File
@@ -1 +0,0 @@
090d82eab24cdd1af43b82f1610e88d90ba4d6c1335660c3bb953b4ad6dfc50c pad2c-1.0.0.elf
-1
View File
@@ -1 +0,0 @@
9341757afec92e1adc3bf7e3978626548fe2a8dceb6c54544bb4f85b7f704a4f pad2c-probe.elf
Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

+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
+3 -3
View File
@@ -1,4 +1,4 @@
/* pad2c's settings, /data/pad2c/config: a line per setting, set from the
/* padbridge's settings, /data/padbridge/config: a line per setting, set from the
* web page (web.c) or by hand.
*
* swap <controller name> A/B and X/Y exchanged for that controller
@@ -8,8 +8,8 @@
* a swap set there holds for whatever controller is connected to it (a
* Nintendo-layout pad such as the GameSir G8 in Switch mode needs it). Lines
* it does not know are kept as they are. */
#ifndef PAD2C_CONFIG_H
#define PAD2C_CONFIG_H
#ifndef PADBRIDGE_CONFIG_H
#define PADBRIDGE_CONFIG_H
#include "pad.h"
+2 -2
View File
@@ -1,6 +1,6 @@
/* The part of AnyPad PS5's host.h that ps5_vpad.c needs. */
#ifndef PAD2C_HOST_H
#define PAD2C_HOST_H
#ifndef PADBRIDGE_HOST_H
#define PADBRIDGE_HOST_H
#include "pad.h"
+5
View File
@@ -53,6 +53,11 @@ static int lock_is_live(const char *path, long boot)
return kill((pid_t)pid, 0) == 0 || errno == EPERM;
}
int lock_held(const char *path)
{
return lock_is_live(path, lock_boot_time());
}
int lock_take(const char *path)
{
char buf[64];
+1
View File
@@ -11,6 +11,7 @@
/* Returns 1 with the lock held, 0 if another copy is running or the file
* cannot be created. */
int lock_take(const char *path);
int lock_held(const char *path); /* 1 if a running copy holds the lock at path */
void lock_release(void);
/* The boot time of this machine, in seconds since 1970; 0 if unknown. */
+153 -27
View File
@@ -1,23 +1,29 @@
/* pad2c -- the 8BitDo Ultimate 2C 2.4G dongle as a second DualSense on a
* jailbroken PS5, read over USB (no Bluetooth involved).
/* padbridge -- USB controllers as virtual DualSense pads on a jailbroken PS5:
* the 8BitDo Ultimate 2C 2.4G dongle, the 8BitDo USB Wireless Adapter 2 (and
* whatever Bluetooth controller is paired with it) and wired XInput pads,
* up to four at once. The console's Bluetooth is never involved.
*
* usbpad.c the dongle, read from its /dev/ugen node
* ps5_vpad.c the virtual DualSense it drives (from AnyPad PS5, GPL-3.0)
* usbpad.c each pad, read from its /dev/ugen node
* ps5_vpad.c the virtual DualSense pads (from AnyPad PS5, GPL-3.0)
* web.c the settings page, port 8090
*
* A virtual pad exists only while the controller is connected to the dongle:
* it is made on the first controller report and removed when the dongle goes
* A virtual pad exists only while its controller is connected: it is made on
* the first controller report and removed when the dongle or adapter goes
* back to "IDLE" (controller off or asleep) or is unplugged.
*
* "Turn off" from the console's menu removes the virtual pad, as a DualSense
* disconnects; Home (PS) brings it back, as it switches a DualSense on.
*
* Stop: create /data/pad2c/stop. It also stops by itself before rest mode.
* Log: /data/pad2c/pad2c.log
* Stop: create /data/padbridge/stop. In rest mode it lets the controllers go
* and waits; when the console wakes, it looks for them again.
* Log: /data/padbridge/padbridge.log
* Up to 1.1.0 this was pad2c, in /data/pad2c: moved here on the first start.
*/
#include "host.h"
#include "lock.h"
#include "log.h"
#include "ps5_power.h"
#include "combo.h"
#include "config.h"
#include "ps5_vpad.h"
#include "usbpad.h"
@@ -33,11 +39,12 @@
#include <string.h>
#include <unistd.h>
#define STATE_DIR "/data/pad2c"
#define LOG_PATH STATE_DIR "/pad2c.log"
#define LOCK_PATH STATE_DIR "/pad2c.lock"
#define STATE_DIR "/data/padbridge"
#define LOG_PATH STATE_DIR "/padbridge.log"
#define LOCK_PATH STATE_DIR "/padbridge.lock"
#define STOP_FLAG STATE_DIR "/stop"
#define CONFIG_PATH STATE_DIR "/config"
#define OLD_DIR "/data/pad2c" /* 1.1.0 and before */
#define T_SCAN 100 /* ms between looks for new pads (the USB Wireless Adapter 2 shows
its XInput identity for under a second) */
#define T_RESEND 50 /* ms: the last state is sent again at least this often */
@@ -63,12 +70,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)
{
@@ -101,10 +138,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];
@@ -142,23 +191,88 @@ 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 */
}
}
/* Up to 1.1.0 this was pad2c. A pad2c still running (from an autoload list)
* is asked to stop, so that two do not read the same pads, and its state
* (remembered users, settings, log) moves here once. Returns what to log. */
static const char *from_pad2c(void)
{
struct stat sb;
int i, asked = 0;
if (stat(OLD_DIR, &sb) != 0) return NULL;
if (lock_held(OLD_DIR "/pad2c.lock")) {
FILE *f = fopen(OLD_DIR "/stop", "w");
if (f) fclose(f);
for (i = 0; i < 50 && lock_held(OLD_DIR "/pad2c.lock"); i++) usleep(100000);
asked = 1;
}
if (stat(STATE_DIR, &sb) == 0) return asked ? "pad2c was running: asked to stop" : NULL;
if (rename(OLD_DIR, STATE_DIR) != 0) return "pad2c's state could not be moved here";
unlink(STATE_DIR "/pad2c.lock");
unlink(STATE_DIR "/stop");
rename(STATE_DIR "/pad2c.log", STATE_DIR "/padbridge.log");
rename(STATE_DIR "/pad2c.log.1", STATE_DIR "/padbridge.log.1");
return asked ? "pad2c was running: stopped, and its state moved here" : "pad2c's state moved here";
}
/* Every virtual pad removed, no pad left rumbling, every USB pad closed. */
static void release_all(void)
{
int i;
for (i = 0; i < HOST_MAX_PADS; i++) {
ctl *c = &g_ctl[i];
if (c->vpad_made) vpad_remove(i);
if (c->pad && usbpad_can_rumble(c->pad)) {
pad_state st;
int k;
usbpad_rumble(c->pad, 0, 0);
for (k = 0; k < 10; k++) {
usbpad_poll(c->pad, &st);
usleep(2000);
}
}
usbpad_close(c->pad);
memset(c, 0, sizeof *c);
pad_state_reset(&c->st);
}
}
/* Waits out rest mode. 1 when the console is working again, 0 if asked to
* stop meanwhile. */
static int rest_wait(void)
{
for (;;) {
int ps = power_state();
if (g_stop) return 0;
if (access(STOP_FLAG, F_OK) == 0) {
unlink(STOP_FLAG);
return 0;
}
if (ps == POWER_WORKING) return 1;
usleep(500000);
}
}
int main(void)
{
const char *moved;
const char *why = "stop requested";
long last_scan = -T_SCAN, last_power = 0, last_check = 0;
int i;
moved = from_pad2c();
mkdir(STATE_DIR, 0777);
log_open(LOG_PATH);
log_line("pad2c %s", PAD2C_VERSION);
log_line("padbridge %s", PADBRIDGE_VERSION);
if (moved) log_line("%s", moved);
if (!lock_take(LOCK_PATH)) {
log_line("another instance is running");
notify("Already running");
@@ -166,7 +280,7 @@ int main(void)
return 1;
}
unlink(STOP_FLAG);
syscall(SYS_thr_set_name, -1, "pad2c");
syscall(SYS_thr_set_name, -1, "padbridge");
signal(SIGTERM, on_signal);
signal(SIGINT, on_signal);
signal(SIGHUP, on_signal);
@@ -183,9 +297,9 @@ int main(void)
{
char ip[32];
if (web_start(WEB_PORT) && web_local_ip(ip, sizeof ip))
notify("Version %s running\nSettings: http://%s:%d", PAD2C_VERSION, ip, WEB_PORT);
notify("Version %s running\nSettings: http://%s:%d", PADBRIDGE_VERSION, ip, WEB_PORT);
else
notify("Version %s running\nSwitch the controller on with its dongle plugged in", PAD2C_VERSION);
notify("Version %s running\nSwitch the controller on with its dongle plugged in", PADBRIDGE_VERSION);
}
for (i = 0; i < HOST_MAX_PADS; i++) pad_state_reset(&g_ctl[i].st);
@@ -200,14 +314,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];
@@ -221,6 +341,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;
@@ -232,8 +353,16 @@ int main(void)
int ps = power_state();
last_power = now;
if (ps == POWER_GOING_TO_REST || ps == POWER_IN_REST) {
why = "rest mode";
break;
/* Rest mode: the pads are let go and padbridge waits, so that
* it is there again when the console wakes (an autoload list
* only runs at a cold start). */
log_line("rest mode: controllers let go, waiting for the console to wake");
release_all();
if (!rest_wait()) break;
vpad_shutdown(); /* the kernel log is opened anew when needed */
last_scan = -T_SCAN;
log_line("awake: looking for controllers again");
continue;
}
}
if (now - last_check >= 1000) {
@@ -247,10 +376,7 @@ 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);
usbpad_close(g_ctl[i].pad);
}
release_all();
web_stop();
vpad_shutdown();
vpad_creds_restore();
+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
+5 -5
View File
@@ -1,4 +1,4 @@
/* pad2c probe -- read-only look at the console's USB devices.
/* padbridge probe -- read-only look at the console's USB devices.
*
* For every /dev/ugenB.A that is not a hub and not the internal Bluetooth
* chip (ugen0.2, left alone on purpose): device descriptor, name, full
@@ -7,7 +7,7 @@
* its interrupt IN endpoints send (press buttons and move the sticks then).
* Also copies what /dev/klog holds, to see why another payload died.
*
* Everything goes to stdout (the loader's socket) and /data/pad2c/probe.log.
* Everything goes to stdout (the loader's socket) and /data/padbridge/probe.log.
* Nothing is written to any device except standard GET_DESCRIPTOR requests
* (configuration, HID report and string descriptors).
*/
@@ -31,7 +31,7 @@
#include <time.h>
#include <unistd.h>
#define DIR_PATH "/data/pad2c"
#define DIR_PATH "/data/padbridge"
#define LOG_PATH DIR_PATH "/probe.log"
#define KLOG_PATH DIR_PATH "/klog.txt"
#ifndef READ_SECONDS
@@ -70,7 +70,7 @@ static void notify(const char *msg)
notify_request req;
memset(&req, 0, sizeof req);
snprintf(req.message, sizeof req.message, "pad2c probe by Sub\n%s", msg);
snprintf(req.message, sizeof req.message, "padbridge probe by Sub\n%s", msg);
sceKernelSendNotificationRequest(0, &req, sizeof req, 0);
}
@@ -398,7 +398,7 @@ int main(void)
mkdir(DIR_PATH, 0777);
g_log = fopen(LOG_PATH, "w");
fw = kernel_get_fw_version();
out("pad2c probe 2, firmware %x.%02x (raw %#x), pid %d", (unsigned)(fw >> 24) & 0xFF,
out("padbridge probe 2, firmware %x.%02x (raw %#x), pid %d", (unsigned)(fw >> 24) & 0xFF,
(unsigned)(fw >> 16) & 0xFF, (unsigned)fw, (int)getpid());
notify("Starting");
copy_klog();
+29 -15
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). */
@@ -111,18 +112,18 @@ typedef struct {
pad_state last; /* the pad's latest state, resent while PS is held from the menu */
long ps_until; /* ms; PS is added to the frames until then (0: not held) */
int ps_active; /* a frame with PS went out and the release is still to be sent */
int sys_off; /* pad2c: the system removed it or it refused input ("turned off") */
int sys_off; /* padbridge: the system removed it or it refused input ("turned off") */
} vslot;
static mbus_bind_fn g_bind;
static int g_ready; /* vpad_init succeeded */
static int32_t g_user = -1;
/* pad2c: the user the pad was last given to, from the console's own choice
/* padbridge: the user the pad was last given to, from the console's own choice
* (the "who is using this controller" screen) as the kernel log tells it.
* Kept in /data/pad2c/owner, so the pad goes back to that user next time, as
* Kept in /data/padbridge/owner, so the pad goes back to that user next time, as
* a DualSense does. -1: none yet. */
#define OWNER_PATH "/data/pad2c/owner" /* slot 0; slot n: owner.n */
#define OWNER_PATH "/data/padbridge/owner" /* slot 0; slot n: owner.n */
static int32_t g_owner[HOST_MAX_PADS] = { -1, -1, -1, -1 };
static void owner_path(int slot, char *out, size_t size)
@@ -193,8 +194,8 @@ 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";
/* Every notification: "padbridge by <author>", then what happened. */
static const char head[] = "padbridge by " PADBRIDGE_AUTHOR "\n";
notify_request req;
va_list ap;
@@ -282,7 +283,7 @@ static uint64_t virtual_device_in(const char *line)
return hex_after(line, "DeviceId");
}
/* pad2c: the source is kept open for as long as the payload runs, so what the
/* padbridge: the source is kept open for as long as the payload runs, so what the
* system does with our pads is seen. Whatever it held before is old news, and
* an old DEVICE_ADDED line must never be taken for ours: drop it all. */
static void log_source_drain(void)
@@ -296,7 +297,7 @@ static void log_source_drain(void)
log_line("vpad: kernel log open (%s), %ld old bytes skipped", g_log_is_dev ? "/dev/klog" : "server on 3232", total);
}
/* pad2c: lines about one of our live pads. The console's choice of user for
/* padbridge: lines about one of our live pads. The console's choice of user for
* it is remembered; its removal by the system (not by us: our own removals
* forget the device first) is the console turning the pad off. Everything
* else said about it is logged, to learn what the console asks of it. */
@@ -396,12 +397,12 @@ int32_t vpad_choose_user(const int32_t login[4], int32_t init, int32_t fore, con
return -1;
}
/* pad2c: whether /data/pad2c/bind_user asks to bind the pad to a user
/* padbridge: whether /data/padbridge/bind_user asks to bind the pad to a user
* ("main" for the default choice, "other", or a hexadecimal id). */
static int bind_wanted(void)
{
char want[40] = "";
FILE *f = fopen("/data/pad2c/bind_user", "r");
FILE *f = fopen("/data/padbridge/bind_user", "r");
if (!f) return 0;
if (!fgets(want, sizeof want, f)) want[0] = '\0';
@@ -420,7 +421,7 @@ static int32_t main_user(void)
if (sceUserServiceGetLoginUserIdList(login) != 0) login[0] = login[1] = login[2] = login[3] = -1;
if (sceUserServiceGetInitialUser(&init) != 0) init = -1;
if (sceUserServiceGetForegroundUser(&fore) != 0) fore = -1;
if ((f = fopen("/data/pad2c/bind_user", "r"))) {
if ((f = fopen("/data/padbridge/bind_user", "r"))) {
if (fgets(want, sizeof want, f)) {
for (i = 0; want[i]; i++) if (want[i] == '\n' || want[i] == '\r' || want[i] == ' ') want[i] = 0;
if (strncmp(want, "0x", 2) == 0) memmove(want, want + 2, strlen(want + 2) + 1);
@@ -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;
@@ -652,9 +666,9 @@ static void finish_identification(long now)
memset(v, 0, sizeof *v);
return;
}
/* pad2c: the user, as a DualSense does it: back to the user who last used
/* padbridge: the user, as a DualSense does it: back to the user who last used
* it, if signed in; otherwise nobody, and the console asks who uses it
* when its PS (Home) button is pressed. /data/pad2c/bind_user = main,
* when its PS (Home) button is pressed. /data/padbridge/bind_user = main,
* other or a hexadecimal id binds it to that user instead. */
{
int32_t to = -1;
@@ -679,7 +693,7 @@ void vpad_poll(long now)
if (!g_ready) return;
ps_tick(now);
log_source_read(); /* pad2c: always, to follow what the console does with our pads */
log_source_read(); /* padbridge: always, to follow what the console does with our pads */
if (g_pending >= 0) {
finish_identification(now);
return;
@@ -740,7 +754,7 @@ static void send_frame(int slot, const pad_state *st)
if (scePadVirtualDeviceInsertData(g_slot[slot].handle, &d) != 0) {
/* Stop feeding a device that refuses: it may not be ours any more. */
log_line("slot %d: the virtual pad refused input, stopped feeding it", slot);
g_slot[slot].sys_off = 1; /* pad2c: as if turned off; Home brings a new one */
g_slot[slot].sys_off = 1; /* padbridge: as if turned off; Home brings a new one */
}
}
+7 -3
View File
@@ -41,16 +41,20 @@ void vpad_update(int slot, const pad_state *st);
int vpad_press_ps(int slot, int ms);
void vpad_remove(int slot);
/* pad2c: 1 when the slot's virtual pad is gone on the console's side (turned
/* padbridge: 1 when the slot's virtual pad is gone on the console's side (turned
* off from the console's menu, refused input, or never identified). The
* caller removes it and asks for a new one when the user presses PS (Home). */
int vpad_off(int slot);
/* pad2c: the user the slot's live pad is given to, -1 if none (or not known). */
/* 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 */
/* pad2c: closes the kernel log, held open while running. */
/* padbridge: closes the kernel log, held open while running. */
void vpad_shutdown(void);
/* The user's own DualSense, read only. Returns 0 if it cannot be read;
+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);
+7 -5
View File
@@ -1,7 +1,7 @@
/* Which interface of a USB device pad2c reads, from its configuration
/* Which interface of a USB device padbridge reads, from its configuration
* descriptor alone (no device needed, so it runs in the tests too). */
#ifndef PAD2C_USBDESC_H
#define PAD2C_USBDESC_H
#ifndef PADBRIDGE_USBDESC_H
#define PADBRIDGE_USBDESC_H
#include <stddef.h>
#include <stdint.h>
@@ -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. */
+197 -8
View File
@@ -1,5 +1,6 @@
#include "usbpad.h"
#include "log.h"
#include "mapswitch.h"
#include "usbdesc.h"
#include "util.h"
@@ -30,6 +31,9 @@
* Adapter 2 shows its XInput identity for under a second. */
static unsigned char g_held[N_BUS][N_ADDR];
static struct { ino_t ino; long ctime, seen; unsigned char set; } g_not_pad[N_BUS][N_ADDR];
static char g_reject[96]; /* what try_node last found not to be a pad */
static char g_logged[N_BUS][N_ADDR][96]; /* per node: the last one logged (a Switch Pro pad
not set up enumerates again every few seconds) */
#define T_NOT_PAD 2000 /* ms a node found not to be a pad is left alone, unless it changes */
struct usbpad {
@@ -47,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)
@@ -55,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];
@@ -67,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. */
@@ -165,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;
@@ -187,15 +294,18 @@ 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);
snprintf(g_reject, sizeof g_reject, "%04x:%04x \"%s\"", UGETW(dd.idVendor), UGETW(dd.idProduct), prod);
*not_pad = 1;
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) {
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;
@@ -256,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. */
@@ -266,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. */
@@ -307,6 +418,12 @@ usbpad *usbpad_open(void)
g_not_pad[bus][addr].set = 0;
return u;
}
/* 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 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;
g_not_pad[bus][addr].ino = sb.st_ino;
g_not_pad[bus][addr].ctime = (long)sb.st_ctime;
@@ -322,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)
@@ -352,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");
@@ -360,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) {
+10 -2
View File
@@ -10,11 +10,13 @@
* 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
#ifndef PADBRIDGE_USBPAD_H
#define PADBRIDGE_USBPAD_H
#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" */
+4 -4
View File
@@ -1,7 +1,7 @@
#ifndef PAD2C_VERSION_H
#define PAD2C_VERSION_H
#ifndef PADBRIDGE_VERSION_H
#define PADBRIDGE_VERSION_H
#define PAD2C_VERSION "1.1.0"
#define PAD2C_AUTHOR "Sub" /* every notification starts "pad2c by Sub" */
#define PADBRIDGE_VERSION "1.3.1"
#define PADBRIDGE_AUTHOR "Sub" /* every notification starts "padbridge by Sub" */
#endif
+1 -1
View File
@@ -90,7 +90,7 @@ int web_status_json(const web_pad *pads, int n, char *out, int size)
{
int len, i, first = 1;
len = snprintf(out, (size_t)size, "{\"version\":\"%s\",\"pads\":[", PAD2C_VERSION);
len = snprintf(out, (size_t)size, "{\"version\":\"%s\",\"pads\":[", PADBRIDGE_VERSION);
for (i = 0; i < n && len < size; i++) {
const web_pad *p = &pads[i];
char name[160], user[100];
+3 -3
View File
@@ -1,4 +1,4 @@
/* pad2c's settings page, http://<console>:8090 from any browser on the local
/* padbridge's settings page, http://<console>:8090 from any browser on the local
* network (phone, computer, the console's own).
*
* GET / the page
@@ -7,8 +7,8 @@
*
* Never blocks: web_poll() takes what has arrived and answers what is
* complete, so a slow browser does not delay the pads. */
#ifndef PAD2C_WEB_H
#define PAD2C_WEB_H
#ifndef PADBRIDGE_WEB_H
#define PADBRIDGE_WEB_H
#include "pad.h"
+6 -6
View File
@@ -1,13 +1,13 @@
/* The settings page served by web.c: one file, no outside resources (the
* console may have no internet). It asks /api/status about 10 times a second
* while it is visible. */
#ifndef PAD2C_WEBPAGE_H
#define PAD2C_WEBPAGE_H
#ifndef PADBRIDGE_WEBPAGE_H
#define PADBRIDGE_WEBPAGE_H
static const char WEB_PAGE[] =
"<!doctype html><html lang=en><head><meta charset=utf-8>"
"<meta name=viewport content='width=device-width,initial-scale=1'>"
"<title>pad2c by Sub</title><style>"
"<title>padbridge by Sub</title><style>"
":root{--bg:#0f1115;--card:#181b22;--line:#2a2f3a;--txt:#e8eaf0;--dim:#8a91a0;--on:#3b82f6;--ok:#22c55e}"
"*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--txt);"
"font:15px/1.4 -apple-system,Segoe UI,Roboto,sans-serif}"
@@ -31,9 +31,9 @@ static const char WEB_PAGE[] =
".bar i{display:block;height:100%;background:var(--on);width:0}"
".empty{color:var(--dim);padding:20px}footer{color:var(--dim);font-size:12px;padding:0 20px 20px}"
"</style></head><body>"
"<header><h1>pad2c by Sub</h1><small id=st>connecting...</small></header>"
"<header><h1>padbridge by Sub</h1><small id=st>connecting...</small></header>"
"<main id=m><div class=empty>No controller yet.</div></main>"
"<footer>Settings are kept on the console in /data/pad2c/config.</footer>"
"<footer>Settings are kept on the console in /data/padbridge/config.</footer>"
"<script>"
"var B=[['Cross',0x4000],['Circle',0x2000],['Square',0x8000],['Triangle',0x1000],['L1',0x400],['R1',0x800],"
"['L2',0x100],['R2',0x200],['L3',2],['R3',4],['Create',1],['Options',8],['PS',0x10000],['Touchpad',0x100000],"
@@ -63,7 +63,7 @@ static const char WEB_PAGE[] =
"if(key!==shown){shown=key;document.getElementById('m').innerHTML=j.pads.length?j.pads.map(card).join('')"
":'<div class=empty>No controller yet. Plug in a dongle or adapter and switch the controller on.</div>'}"
"j.pads.forEach(live);setTimeout(tick,100)}).catch(function(){"
"document.getElementById('st').textContent='pad2c is not answering';setTimeout(tick,1000)})}"
"document.getElementById('st').textContent='padbridge is not answering';setTimeout(tick,1000)})}"
"function sw(e){var s=e.getAttribute('data-s');busy[s]=1;"
"fetch('/api/swap?slot='+s+'&on='+(e.checked?1:0),{method:'POST'}).then(function(){"
"setTimeout(function(){busy[s]=0},300)})}"
+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;
}
+1 -1
View File
@@ -31,7 +31,7 @@ int main(void)
/* lines it does not know are kept, a damaged file is no harm */
f = fopen(path, "w");
fputs("# pad2c settings\nfuture-option 7\nswap Some Pad\n\nswap\n", f);
fputs("# padbridge settings\nfuture-option 7\nswap Some Pad\n\nswap\n", f);
fclose(f);
config_load(path);
CHECK(config_swap("Some Pad"), "swap from a hand-written file");
+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;
}
+10 -10
View File
@@ -1,7 +1,7 @@
/* 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. */
* The 310a descriptor is the one in issue #2 (padbridge-probe on firmware 13.00):
* the Ultimate 2C receiver in XInput mode. padbridge 0.2.1 read its keyboard. */
#include "usbdesc.h"
#include "usbpad.h"
@@ -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);
}