mirror of
https://github.com/sub0x00001/pad2c.git
synced 2026-10-23 22:01:55 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d97019644 | ||
|
|
41bece1b31 | ||
|
|
f29a1de877 |
No files matched your search
@@ -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
@@ -1,4 +1,3 @@
|
||||
build/
|
||||
dist/*.elf
|
||||
!dist/.keep
|
||||
dist/
|
||||
release/
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
<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://<console IP>: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://<console IP>: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.
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
090d82eab24cdd1af43b82f1610e88d90ba4d6c1335660c3bb953b4ad6dfc50c pad2c-1.0.0.elf
|
||||
Vendored
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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];
|
||||
|
||||
@@ -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
@@ -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)})}"
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user