mirror of
https://github.com/sub0x00001/pad2c.git
synced 2026-10-23 23:02:30 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d97019644 | ||
|
|
41bece1b31 | ||
|
|
f29a1de877 | ||
|
|
70f3419796 |
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,58 @@
|
||||
# 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
|
||||
host answers like Windows: pad2c now looks for new pads every 100 ms and sends XInput pads what Windows sends (the
|
||||
Microsoft OS string and the XUSB request) before the LED, so the adapter settles in XInput by itself instead of its
|
||||
DualShock 4 mode, which stops sending after a moment. DInput (View + Left) works too.
|
||||
- **Several controllers at once**, up to four, each its own virtual DualSense and its own remembered user
|
||||
(`/data/pad2c/owner`, `owner.1`, ...).
|
||||
- The keyboard and mouse the adapter (and the 2C receiver in XInput mode) add beside the pad are detached from the
|
||||
console's driver: the console lists only the controller among its accessories.
|
||||
- **Settings page** on port 8090 (address in the start notification): the controllers live, and a **Nintendo
|
||||
layout** switch per controller (A/B and X/Y swapped, kept in `/data/pad2c/config`) for pads such as the G8 in
|
||||
Switch mode.
|
||||
- DInput: a DualSense's touchpad click through the adapter.
|
||||
- Wired XInput controllers (tested: GameSir G8 on its cable) work as well.
|
||||
- The adapter is named "8BitDo USB Wireless Adapter 2" in the notifications.
|
||||
- `pad2c-probe`: the product and manufacturer names, the device release, 90 s of reports.
|
||||
|
||||
## 1.0.0
|
||||
- The Ultimate 2C receiver in XInput mode works (issue #2). When the controller connects, that receiver enumerates
|
||||
as `2dc8:310a` with an XInput interface (`ff/5d/01`), a keyboard and a vendor HID; 0.2.1 read the keyboard. pad2c
|
||||
|
||||
@@ -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
|
||||
@@ -13,6 +13,14 @@ test:
|
||||
./build/test_map
|
||||
cc -std=c11 -Wall -Wextra -Werror -Isrc -o build/test_usb src/usbdesc.c src/mapxinput.c tests/test_usb.c
|
||||
./build/test_usb
|
||||
cc -std=c11 -Wall -Wextra -Werror -Isrc -o build/test_config src/config.c tests/test_config.c
|
||||
./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
|
||||
@@ -21,15 +29,15 @@ 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/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
|
||||
dist/padbridge-probe.elf: src/probe.c src/usbdesc.c src/usbdesc.h
|
||||
@mkdir -p dist
|
||||
$(CC) $(CFLAGS) -o $@ $^
|
||||
$(CC) $(CFLAGS) -o $@ src/probe.c src/usbdesc.c
|
||||
|
||||
clean:
|
||||
rm -rf dist build
|
||||
|
||||
@@ -1,129 +1,225 @@
|
||||
# pad2c
|
||||
# padbridge
|
||||
|
||||
**Use an [8BitDo Ultimate 2C Wireless Controller](https://www.8bitdo.com/ultimate-2c-wireless-controller/) on a
|
||||
jailbroken PS5, as a second controller next to your DualSense.**
|
||||
**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 plugged into the console's USB port and
|
||||
shows it to the system and to games as a **virtual DualSense**. No Bluetooth is involved, so it never touches your
|
||||
DualSense's pairing.
|
||||
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 |
|
||||
| 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 Bluetooth at all.
|
||||
|
||||
**Which firmware?** Tested on 13.60 (PS5 Slim). It needs a console that already runs a payload loader such as elfldr
|
||||
or Payload Manager; it does not jailbreak anything itself.
|
||||
|
||||
## Status
|
||||
|
||||
| | |
|
||||
| Controller | Rumbles |
|
||||
|---|---|
|
||||
| Tested on | PS5 Slim (CFI-2114), firmware **13.60**, elfldr 0.26, kstuff-lite 1.11, Payload Manager 0.5.2 |
|
||||
| Controller | 8BitDo Ultimate 2C Wireless, 2.4G dongle, DInput (`2dc8:301c`) and XInput (`2dc8:310a`) modes |
|
||||
| Works | all buttons, sticks, analog triggers, d-pad, Home = PS, console menus and games, sleep and wake, the console's "who is using this controller" screen, remembers its user, "turn off" from the console's menu |
|
||||
| Not yet | vibration, light bar, motion sensors, audio |
|
||||
| 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.
|
||||
|
||||
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 into a USB port of the PS5 and switch the controller on in 2.4G mode.
|
||||
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. Press **Home** on the 8BitDo: the console asks who is using the controller.
|
||||
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/`:
|
||||
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 (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 | |
|
||||
|---|---|
|
||||
| `pad2c.log` | the log |
|
||||
| `stop` | create it to stop pad2c (it also stops by itself before rest mode) |
|
||||
| `owner` | the user the controller was last given to (delete it to forget) |
|
||||
| `bind_user` | optional: `main`, `other` or a hexadecimal user id, to give the controller to that user at once |
|
||||
| `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 the dongle's `/dev/ugenB.A` node and reads its interrupt IN endpoint. The console's own HID
|
||||
driver holds the interface but ignores the device. The internal Bluetooth chip (`ugen0.2`) is never touched.
|
||||
- With no controller the dongle shows up as "8BitDo IDLE". When the controller connects, the dongle enumerates again:
|
||||
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/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
|
||||
|
||||
@@ -132,8 +228,8 @@ Needs [ps5-payload-sdk](https://github.com/ps5-payload-dev/sdk) and clang, lld a
|
||||
|
||||
```
|
||||
. ~/.ps5sdk
|
||||
make test # the button maps and the interface choice, on the PC
|
||||
make # dist/pad2c-<version>.elf and dist/pad2c-probe.elf
|
||||
make test # button maps, interface choice, settings, status JSON, on the PC
|
||||
make # dist/padbridge-<version>.elf and dist/padbridge-probe.elf
|
||||
```
|
||||
|
||||
## Credits
|
||||
@@ -141,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.
|
||||
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
|
||||
@@ -0,0 +1,84 @@
|
||||
#include "config.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define MAX_LINES 64
|
||||
#define LINE_LEN 96
|
||||
|
||||
static char g_path[128];
|
||||
static char g_lines[MAX_LINES][LINE_LEN]; /* the file as read, "swap ..." lines included */
|
||||
static int g_n;
|
||||
|
||||
void config_load(const char *path)
|
||||
{
|
||||
FILE *f;
|
||||
char line[LINE_LEN];
|
||||
|
||||
snprintf(g_path, sizeof g_path, "%s", path);
|
||||
g_n = 0;
|
||||
if (!(f = fopen(path, "r"))) return;
|
||||
while (g_n < MAX_LINES && fgets(line, sizeof line, f)) {
|
||||
line[strcspn(line, "\r\n")] = 0;
|
||||
snprintf(g_lines[g_n++], LINE_LEN, "%s", line);
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
static int is_swap_of(const char *line, const char *name)
|
||||
{
|
||||
return !strncmp(line, "swap ", 5) && !strcmp(line + 5, name);
|
||||
}
|
||||
|
||||
int config_swap(const char *name)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < g_n; i++)
|
||||
if (is_swap_of(g_lines[i], name)) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int save(void)
|
||||
{
|
||||
char tmp[sizeof g_path + 4];
|
||||
FILE *f;
|
||||
int i;
|
||||
|
||||
snprintf(tmp, sizeof tmp, "%s.tmp", g_path);
|
||||
if (!(f = fopen(tmp, "w"))) return 0;
|
||||
for (i = 0; i < g_n; i++) fprintf(f, "%s\n", g_lines[i]);
|
||||
if (fclose(f) != 0) return 0;
|
||||
return rename(tmp, g_path) == 0;
|
||||
}
|
||||
|
||||
int config_set_swap(const char *name, int on)
|
||||
{
|
||||
int i;
|
||||
|
||||
if (!name[0] || strlen(name) + 6 > LINE_LEN) return 0;
|
||||
if (on == config_swap(name)) return 1;
|
||||
if (on) {
|
||||
if (g_n == MAX_LINES) return 0;
|
||||
snprintf(g_lines[g_n++], LINE_LEN, "swap %s", name);
|
||||
} else {
|
||||
for (i = 0; i < g_n; i++)
|
||||
if (is_swap_of(g_lines[i], name)) {
|
||||
memmove(g_lines[i], g_lines[i + 1], (size_t)(g_n - i - 1) * LINE_LEN);
|
||||
g_n--;
|
||||
i--;
|
||||
}
|
||||
}
|
||||
return save();
|
||||
}
|
||||
|
||||
void config_apply_swap(pad_state *st)
|
||||
{
|
||||
uint32_t b = st->buttons, out = b & ~(uint32_t)(PAD_CROSS | PAD_CIRCLE | PAD_SQUARE | PAD_TRIANGLE);
|
||||
|
||||
if (b & PAD_CROSS) out |= PAD_CIRCLE;
|
||||
if (b & PAD_CIRCLE) out |= PAD_CROSS;
|
||||
if (b & PAD_SQUARE) out |= PAD_TRIANGLE;
|
||||
if (b & PAD_TRIANGLE) out |= PAD_SQUARE;
|
||||
st->buttons = out;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/* 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
|
||||
*
|
||||
* A controller is known by its name (usbpad_name), the same in all its
|
||||
* modes: through the 8BitDo USB Wireless Adapter 2 that is the adapter's, so
|
||||
* 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 PADBRIDGE_CONFIG_H
|
||||
#define PADBRIDGE_CONFIG_H
|
||||
|
||||
#include "pad.h"
|
||||
|
||||
void config_load(const char *path);
|
||||
int config_swap(const char *name);
|
||||
int config_set_swap(const char *name, int on); /* saved at once; 0 if it could not be */
|
||||
|
||||
/* Cross with circle, square with triangle. */
|
||||
void config_apply_swap(pad_state *st);
|
||||
|
||||
#endif
|
||||
+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. */
|
||||
|
||||
+287
-76
@@ -1,26 +1,35 @@
|
||||
/* 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"
|
||||
#include "util.h"
|
||||
#include "version.h"
|
||||
#include "web.h"
|
||||
|
||||
#include <sys/stat.h>
|
||||
#include <sys/syscall.h>
|
||||
@@ -30,12 +39,14 @@
|
||||
#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 SLOT 0
|
||||
#define T_SCAN 1000 /* ms between looks for the dongle */
|
||||
#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 */
|
||||
/* No silence timeout: the dongle only reports changes, so a pad held still is
|
||||
* silent. When the controller sleeps or switches off, the dongle enumerates
|
||||
@@ -49,20 +60,219 @@ static void on_signal(int sig)
|
||||
g_stop = 1;
|
||||
}
|
||||
|
||||
/* One USB pad and the virtual DualSense it drives: slot i of ps5_vpad.c. */
|
||||
typedef struct {
|
||||
usbpad *pad;
|
||||
pad_state st;
|
||||
long last_sent;
|
||||
int vpad_made; /* a virtual pad was asked for and not removed since */
|
||||
int turned_off; /* the console turned it off: Home brings it back */
|
||||
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)
|
||||
{
|
||||
ctl *c = &g_ctl[slot];
|
||||
const char *name;
|
||||
|
||||
if (!c->pad) return;
|
||||
name = usbpad_name(c->pad);
|
||||
c->swap = on;
|
||||
log_line("slot %d: A/B and X/Y %s for \"%s\"%s", slot, on ? "swapped" : "as they are", name,
|
||||
config_set_swap(name, on) ? "" : " (not saved)");
|
||||
}
|
||||
|
||||
/* What the settings page shows. */
|
||||
static void web_fill(web_pad *w, long now)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < HOST_MAX_PADS; i++) {
|
||||
ctl *c = &g_ctl[i];
|
||||
memset(&w[i], 0, sizeof w[i]);
|
||||
if (!c->pad) continue;
|
||||
if (now - c->user_at >= 2000) {
|
||||
c->user_at = now;
|
||||
if (!c->vpad_made || !vpad_user_name(vpad_user(i), c->user, sizeof c->user)) c->user[0] = 0;
|
||||
}
|
||||
w[i].present = 1;
|
||||
w[i].connected = c->vpad_made;
|
||||
snprintf(w[i].name, sizeof w[i].name, "%s", usbpad_name(c->pad));
|
||||
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->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];
|
||||
int r = usbpad_poll(c->pad, &c->st);
|
||||
|
||||
if (r > 0 && c->swap) config_apply_swap(&c->st);
|
||||
|
||||
if (r < 0) {
|
||||
usbpad_close(c->pad);
|
||||
c->pad = NULL;
|
||||
if (c->vpad_made) {
|
||||
vpad_remove(slot);
|
||||
c->vpad_made = 0;
|
||||
log_line("slot %d: controller left", slot);
|
||||
notify("%s disconnected", c->model);
|
||||
}
|
||||
c->turned_off = 0; /* switched off for real: it starts afresh */
|
||||
pad_state_reset(&c->st);
|
||||
} else if (r > 0) {
|
||||
int ps_new = (c->st.buttons & PAD_PS) && !c->ps_latch;
|
||||
|
||||
if (!(c->st.buttons & PAD_PS)) c->ps_latch = 0;
|
||||
if (!c->vpad_made && c->turned_off) {
|
||||
if (ps_new) { /* that press only switches it on: not passed on */
|
||||
c->vpad_made = vpad_add(slot);
|
||||
c->turned_off = 0;
|
||||
c->ps_latch = 1;
|
||||
log_line("slot %d: PS pressed: virtual pad back on", slot);
|
||||
}
|
||||
} else if (!c->vpad_made) {
|
||||
c->vpad_made = vpad_add(slot);
|
||||
snprintf(c->model, sizeof c->model, "%s (%s)", usbpad_name(c->pad), usbpad_mode(c->pad));
|
||||
log_line("slot %d: controller connected: %s, virtual pad %s", slot, c->model,
|
||||
c->vpad_made ? "requested" : "refused");
|
||||
notify("%s connected", c->model);
|
||||
}
|
||||
if (c->ps_latch) c->st.buttons &= ~(uint32_t)PAD_PS;
|
||||
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)
|
||||
{
|
||||
usbpad *pad = NULL;
|
||||
pad_state st;
|
||||
const char *moved;
|
||||
const char *why = "stop requested";
|
||||
long last_scan = -T_SCAN, last_power = 0, last_check = 0, last_sent = 0;
|
||||
int vpad_made = 0; /* a virtual pad was asked for and not removed since */
|
||||
int turned_off = 0; /* the console turned it off: Home brings it back */
|
||||
int ps_latch = 0; /* PS (Home) held since before: not a new press */
|
||||
char model[80] = "Controller"; /* "<name> (<mode>)" of the last controller, for the notifications */
|
||||
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");
|
||||
@@ -70,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);
|
||||
@@ -83,75 +293,76 @@ int main(void)
|
||||
log_close();
|
||||
return 1;
|
||||
}
|
||||
notify("Version %s running\nSwitch the controller on with its dongle plugged in", PAD2C_VERSION);
|
||||
pad_state_reset(&st);
|
||||
config_load(CONFIG_PATH);
|
||||
{
|
||||
char ip[32];
|
||||
if (web_start(WEB_PORT) && web_local_ip(ip, sizeof ip))
|
||||
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", PADBRIDGE_VERSION);
|
||||
}
|
||||
for (i = 0; i < HOST_MAX_PADS; i++) pad_state_reset(&g_ctl[i].st);
|
||||
|
||||
while (!g_stop) {
|
||||
long now = now_ms();
|
||||
int r = 0;
|
||||
|
||||
if (!pad && now - last_scan >= T_SCAN) {
|
||||
/* New pads, while a slot is free: each gets the first free one. */
|
||||
if (now - last_scan >= T_SCAN) {
|
||||
last_scan = now;
|
||||
pad = usbpad_open();
|
||||
}
|
||||
if (pad) {
|
||||
r = usbpad_poll(pad, &st);
|
||||
if (r < 0) {
|
||||
usbpad_close(pad);
|
||||
pad = NULL;
|
||||
if (vpad_made) {
|
||||
vpad_remove(SLOT);
|
||||
vpad_made = 0;
|
||||
log_line("controller left");
|
||||
notify("%s disconnected", model);
|
||||
for (i = 0; i < HOST_MAX_PADS; i++) {
|
||||
if (g_ctl[i].pad || g_ctl[i].vpad_made) continue;
|
||||
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" : "");
|
||||
}
|
||||
turned_off = 0; /* switched off for real: it starts afresh */
|
||||
pad_state_reset(&st);
|
||||
} else if (r > 0) {
|
||||
int ps_new = (st.buttons & PAD_PS) && !ps_latch;
|
||||
|
||||
if (!(st.buttons & PAD_PS)) ps_latch = 0;
|
||||
if (!vpad_made && turned_off) {
|
||||
if (ps_new) { /* that press only switches it on: not passed on */
|
||||
vpad_made = vpad_add(SLOT);
|
||||
turned_off = 0;
|
||||
ps_latch = 1;
|
||||
log_line("PS pressed: virtual pad back on");
|
||||
}
|
||||
} else if (!vpad_made) {
|
||||
vpad_made = vpad_add(SLOT);
|
||||
snprintf(model, sizeof model, "%s (%s)", usbpad_name(pad), usbpad_mode(pad));
|
||||
log_line("controller connected: %s, virtual pad %s", model,
|
||||
vpad_made ? "requested" : "refused");
|
||||
notify("%s connected", model);
|
||||
}
|
||||
if (ps_latch) st.buttons &= ~(uint32_t)PAD_PS;
|
||||
if (vpad_made) vpad_update(SLOT, &st);
|
||||
last_sent = now;
|
||||
} else if (vpad_made && now - last_sent >= T_RESEND) {
|
||||
vpad_update(SLOT, &st); /* keep the pad fed while nothing changes */
|
||||
last_sent = now;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (i = 0; i < HOST_MAX_PADS; i++)
|
||||
if (g_ctl[i].pad) {
|
||||
ctl_poll(i, now);
|
||||
remote_poll(i, now);
|
||||
}
|
||||
vpad_poll(now);
|
||||
{
|
||||
web_pad w[HOST_MAX_PADS];
|
||||
web_fill(w, now);
|
||||
web_poll(w, HOST_MAX_PADS, set_swap);
|
||||
}
|
||||
|
||||
/* Turned off from the console's menu (or the console dropped it): the
|
||||
* virtual pad goes away, as a DualSense disconnects. */
|
||||
if (vpad_made && vpad_off(SLOT)) {
|
||||
vpad_remove(SLOT);
|
||||
vpad_made = 0;
|
||||
turned_off = 1;
|
||||
ps_latch = (st.buttons & PAD_PS) != 0;
|
||||
log_line("virtual pad turned off; waiting for PS (Home)");
|
||||
notify("%s turned off\nPress Home to turn it back on", model);
|
||||
for (i = 0; i < HOST_MAX_PADS; i++) {
|
||||
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;
|
||||
log_line("slot %d: virtual pad turned off; waiting for PS (Home)", i);
|
||||
notify("%s turned off\nPress Home to turn it back on", c->model);
|
||||
}
|
||||
|
||||
if (now - last_power >= 200) {
|
||||
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) {
|
||||
@@ -161,12 +372,12 @@ int main(void)
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (r <= 0) usleep(2000);
|
||||
usleep(2000);
|
||||
}
|
||||
|
||||
log_line("stopping: %s", why);
|
||||
if (vpad_made) vpad_remove(SLOT);
|
||||
usbpad_close(pad);
|
||||
release_all();
|
||||
web_stop();
|
||||
vpad_shutdown();
|
||||
vpad_creds_restore();
|
||||
lock_release();
|
||||
|
||||
+3
-1
@@ -5,7 +5,8 @@
|
||||
|
||||
/* 8BitDo Ultimate 2C dongle, report 1 (from its HID report descriptor and a
|
||||
* calibration on the console):
|
||||
* [0] 01 [1..2] buttons 1-15 [3] hat (low nibble, 0 up clockwise, 15 none)
|
||||
* [0] 01 [1..2] buttons 1-15 [3] hat (low nibble, 0 up clockwise, 15 none),
|
||||
* bit 4: touchpad click (the USB Wireless Adapter 2 in DInput mode, with a DualSense)
|
||||
* [4] LX [5] LY [6] RX [7] RY (127 centre, up is smaller) [8] RT [9] LT
|
||||
* Buttons: 1 A, 2 B, 3 L4, 4 X, 5 Y, 6 R4, 7 LB, 8 RB, 9 LT, 10 RT,
|
||||
* 11 View, 12 Menu, 13 Home, 14 L3, 15 R3.
|
||||
@@ -32,6 +33,7 @@ int usbpad_parse_8bitdo(const unsigned char *r, int n, pad_state *st)
|
||||
for (i = 0; i < sizeof map / sizeof map[0]; i++)
|
||||
if (b & map[i].bit) st->buttons |= map[i].pad;
|
||||
if ((r[3] & 0x0F) < 8) st->buttons |= hat[r[3] & 0x0F];
|
||||
if (r[3] & 0x10) st->buttons |= PAD_TOUCHPAD; /* USB Wireless Adapter 2: a DualSense's touchpad */
|
||||
st->lx = r[4];
|
||||
st->ly = r[5];
|
||||
st->rx = r[6];
|
||||
|
||||
+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
|
||||
+47
-12
@@ -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,8 +7,9 @@
|
||||
* 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.
|
||||
* Nothing is written to any device except standard GET_DESCRIPTOR requests.
|
||||
* 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).
|
||||
*/
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/stat.h>
|
||||
@@ -19,6 +20,8 @@
|
||||
|
||||
#include <ps5/kernel.h>
|
||||
|
||||
#include "usbdesc.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdarg.h>
|
||||
@@ -28,11 +31,11 @@
|
||||
#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
|
||||
#define READ_SECONDS 60
|
||||
#define READ_SECONDS 90
|
||||
#endif
|
||||
#define MAX_EPS 8
|
||||
|
||||
@@ -67,7 +70,7 @@ static void notify(const char *msg)
|
||||
notify_request req;
|
||||
|
||||
memset(&req, 0, sizeof req);
|
||||
snprintf(req.message, sizeof req.message, "%s", msg);
|
||||
snprintf(req.message, sizeof req.message, "padbridge probe by Sub\n%s", msg);
|
||||
sceKernelSendNotificationRequest(0, &req, sizeof req, 0);
|
||||
}
|
||||
|
||||
@@ -235,7 +238,11 @@ static void read_reports(int fd, const ep_in *eps, int n)
|
||||
}
|
||||
|
||||
out(">>> PRESS BUTTONS, TRIGGERS AND MOVE BOTH STICKS NOW (%d s) <<<", READ_SECONDS);
|
||||
notify("pad2c probe: press every button, one at a time (60 s)");
|
||||
{
|
||||
char msg[96];
|
||||
snprintf(msg, sizeof msg, "Press every button, one at a time (%d s)", READ_SECONDS);
|
||||
notify(msg);
|
||||
}
|
||||
end = now_ms() + READ_SECONDS * 1000;
|
||||
while (now_ms() < end) {
|
||||
struct usb_fs_complete done;
|
||||
@@ -289,6 +296,31 @@ static void read_reports(int fd, const ep_in *eps, int n)
|
||||
}
|
||||
}
|
||||
|
||||
/* A string descriptor (US English), with a standard GET_DESCRIPTOR. The
|
||||
* serial number is not read. */
|
||||
static void print_string(int fd, const char *what, int index)
|
||||
{
|
||||
struct usb_ctl_request req;
|
||||
uint8_t buf[255];
|
||||
char s[128];
|
||||
|
||||
if (index == 0) return;
|
||||
memset(&req, 0, sizeof req);
|
||||
memset(buf, 0, sizeof buf);
|
||||
req.ucr_data = buf;
|
||||
req.ucr_flags = 0x0004; /* USB_SHORT_XFER_OK */
|
||||
req.ucr_request.bmRequestType = UT_READ_DEVICE;
|
||||
req.ucr_request.bRequest = UR_GET_DESCRIPTOR;
|
||||
USETW(req.ucr_request.wValue, 0x0300 | index);
|
||||
USETW(req.ucr_request.wIndex, 0x0409);
|
||||
USETW(req.ucr_request.wLength, sizeof buf);
|
||||
if (ioctl(fd, USB_DO_REQUEST, &req) != 0) {
|
||||
out(" %s string %d: errno %d", what, index, errno);
|
||||
return;
|
||||
}
|
||||
if (usbdesc_string(buf, req.ucr_actlen, s, sizeof s)) out(" %s \"%s\"", what, s);
|
||||
}
|
||||
|
||||
static void probe_device(int bus, int addr)
|
||||
{
|
||||
char path[32];
|
||||
@@ -316,13 +348,16 @@ static void probe_device(int bus, int addr)
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
out("%s: %04x:%04x class %02x/%02x/%02x USB %04x", path, UGETW(dd.idVendor), UGETW(dd.idProduct),
|
||||
dd.bDeviceClass, dd.bDeviceSubClass, dd.bDeviceProtocol, UGETW(dd.bcdUSB));
|
||||
out("%s: %04x:%04x class %02x/%02x/%02x USB %04x, device release %x.%02x", path, UGETW(dd.idVendor),
|
||||
UGETW(dd.idProduct), dd.bDeviceClass, dd.bDeviceSubClass, dd.bDeviceProtocol, UGETW(dd.bcdUSB),
|
||||
UGETW(dd.bcdDevice) >> 8, UGETW(dd.bcdDevice) & 0xFF);
|
||||
if (dd.bDeviceClass == 0x09) {
|
||||
out(" a hub, skipped");
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
print_string(fd, "manufacturer", dd.iManufacturer);
|
||||
print_string(fd, "product", dd.iProduct);
|
||||
memset(&di, 0, sizeof di);
|
||||
if (ioctl(fd, USB_GET_DEVICEINFO, &di) == 0)
|
||||
out(" \"%.60s\" by \"%.60s\", speed %u, config %u", di.udi_product, di.udi_vendor, di.udi_speed,
|
||||
@@ -363,14 +398,14 @@ int main(void)
|
||||
mkdir(DIR_PATH, 0777);
|
||||
g_log = fopen(LOG_PATH, "w");
|
||||
fw = kernel_get_fw_version();
|
||||
out("pad2c probe 1, 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("pad2c probe: starting");
|
||||
notify("Starting");
|
||||
copy_klog();
|
||||
for (bus = 0; bus < 4; bus++)
|
||||
for (addr = 1; addr < 10; addr++) probe_device(bus, addr);
|
||||
out("done");
|
||||
notify("pad2c probe: done");
|
||||
notify("Done");
|
||||
if (g_log) fclose(g_log);
|
||||
return 0;
|
||||
}
|
||||
+75
-26
@@ -69,6 +69,7 @@ int32_t sceUserServiceInitialize(void *params);
|
||||
int32_t sceUserServiceGetInitialUser(int32_t *user);
|
||||
int32_t sceUserServiceGetForegroundUser(int32_t *user);
|
||||
int32_t sceUserServiceGetLoginUserIdList(int32_t list[4]);
|
||||
int32_t sceUserServiceGetUserName(int32_t user, char *name, size_t size);
|
||||
int32_t scePadInit(void);
|
||||
int32_t scePadSetProcessPrivilege(int32_t privilege);
|
||||
int32_t scePadGetHandle(int32_t user, int32_t type, int32_t index);
|
||||
@@ -77,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). */
|
||||
@@ -110,40 +112,54 @@ 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"
|
||||
static int32_t g_owner = -1;
|
||||
#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)
|
||||
{
|
||||
if (slot == 0) snprintf(out, size, "%s", OWNER_PATH);
|
||||
else snprintf(out, size, "%s.%d", OWNER_PATH, slot);
|
||||
}
|
||||
|
||||
static void owner_load(void)
|
||||
{
|
||||
FILE *f = fopen(OWNER_PATH, "r");
|
||||
char path[64];
|
||||
unsigned v;
|
||||
int i;
|
||||
|
||||
if (!f) return;
|
||||
if (fscanf(f, "%x", &v) == 1 && v != 0 && v != 0xFFFFFFFFu) g_owner = (int32_t)v;
|
||||
fclose(f);
|
||||
for (i = 0; i < HOST_MAX_PADS; i++) {
|
||||
FILE *f;
|
||||
owner_path(i, path, sizeof path);
|
||||
if (!(f = fopen(path, "r"))) continue;
|
||||
if (fscanf(f, "%x", &v) == 1 && v != 0 && v != 0xFFFFFFFFu) g_owner[i] = (int32_t)v;
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
static void owner_save(int slot, int32_t user)
|
||||
{
|
||||
char path[64], tmp[72];
|
||||
FILE *f;
|
||||
|
||||
if (user == g_owner) return;
|
||||
g_owner = user;
|
||||
if ((f = fopen(OWNER_PATH ".tmp", "w"))) {
|
||||
if (slot < 0 || slot >= HOST_MAX_PADS || user == g_owner[slot]) return;
|
||||
g_owner[slot] = user;
|
||||
owner_path(slot, path, sizeof path);
|
||||
snprintf(tmp, sizeof tmp, "%s.tmp", path);
|
||||
if ((f = fopen(tmp, "w"))) {
|
||||
fprintf(f, "%x\n", (unsigned)user);
|
||||
fclose(f);
|
||||
rename(OWNER_PATH ".tmp", OWNER_PATH);
|
||||
rename(tmp, path);
|
||||
}
|
||||
log_line("slot %d: now used by user %#x, remembered", slot, (unsigned)user);
|
||||
}
|
||||
@@ -178,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;
|
||||
|
||||
@@ -267,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)
|
||||
@@ -281,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. */
|
||||
@@ -381,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';
|
||||
@@ -405,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);
|
||||
@@ -523,18 +539,51 @@ int vpad_init(void)
|
||||
g_user = main_user();
|
||||
log_line("vpad: main user %#x", (unsigned)g_user);
|
||||
owner_load();
|
||||
if (g_owner != -1) log_line("vpad: the pad was last used by user %#x", (unsigned)g_owner);
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < HOST_MAX_PADS; i++)
|
||||
if (g_owner[i] != -1) log_line("vpad: slot %d was last used by user %#x", i, (unsigned)g_owner[i]);
|
||||
}
|
||||
g_log_fd = log_source_open();
|
||||
if (g_log_fd >= 0) log_source_drain();
|
||||
g_ready = 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int vpad_user_name(int32_t user, char *out, size_t size)
|
||||
{
|
||||
out[0] = 0;
|
||||
if (user == -1 || sceUserServiceGetUserName(user, out, size) != 0) {
|
||||
out[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
return out[0] != 0;
|
||||
}
|
||||
|
||||
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;
|
||||
return g_owner[slot];
|
||||
}
|
||||
|
||||
int vpad_off(int slot)
|
||||
{
|
||||
if (slot < 0 || slot >= HOST_MAX_PADS) return 0;
|
||||
@@ -617,14 +666,14 @@ 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;
|
||||
if (bind_wanted()) to = g_user = main_user();
|
||||
else if (g_owner != -1 && signed_in(g_owner)) to = g_owner;
|
||||
else if (g_owner[slot] != -1 && signed_in(g_owner[slot])) to = g_owner[slot];
|
||||
v->st = VP_READY;
|
||||
if (to == -1) {
|
||||
log_line("slot %d: device %#llx left unassigned: press Home on the pad to choose its user", slot,
|
||||
@@ -644,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;
|
||||
@@ -705,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 */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
-2
@@ -22,6 +22,8 @@
|
||||
|
||||
#include "pad.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* Raises this process's credentials (checked), loads MBus and libScePad.
|
||||
* Returns 0 if any step fails; virtual pads then stay off. */
|
||||
int vpad_init(void);
|
||||
@@ -39,12 +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: closes the kernel log, held open while running. */
|
||||
/* 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 */
|
||||
|
||||
/* 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;
|
||||
|
||||
+28
-6
@@ -17,13 +17,22 @@ static int is_xinput(const iface *f)
|
||||
}
|
||||
|
||||
/* HID, but not a boot keyboard (1/1) or mouse (1/2). */
|
||||
static int is_pad_hid(const iface *f)
|
||||
static int is_boot_kbd_mouse(const iface *f)
|
||||
{
|
||||
return f->cls == 0x03 && !(f->sub == 0x01 && (f->proto == 0x01 || f->proto == 0x02));
|
||||
return f->cls == 0x03 && f->sub == 0x01 && (f->proto == 0x01 || f->proto == 0x02);
|
||||
}
|
||||
|
||||
static int pick(const iface *f, int kind, usbdesc_pick *out)
|
||||
static int is_pad_hid(const iface *f)
|
||||
{
|
||||
return f->cls == 0x03 && !is_boot_kbd_mouse(f);
|
||||
}
|
||||
|
||||
static int pick(const iface *ifs, int n, const iface *f, int kind, usbdesc_pick *out)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < n; i++)
|
||||
if (is_boot_kbd_mouse(&ifs[i]) && ifs[i].num < 32) out->hide |= 1u << ifs[i].num;
|
||||
out->kind = kind;
|
||||
out->iface = f->num;
|
||||
out->ep_in = f->ep_in;
|
||||
@@ -33,7 +42,7 @@ static int pick(const iface *f, int kind, usbdesc_pick *out)
|
||||
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;
|
||||
@@ -66,10 +75,13 @@ 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[i], USBPAD_XINPUT, out);
|
||||
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[i], USBPAD_8BITDO, out);
|
||||
if (is_pad_hid(&ifs[i]) && ifs[i].ep_in) return pick(ifs, n, &ifs[i], USBPAD_8BITDO, out);
|
||||
return USBPAD_NONE;
|
||||
}
|
||||
|
||||
@@ -90,3 +102,13 @@ int usbdesc_string(const unsigned char *d, int len, char *out, size_t size)
|
||||
out[n] = 0;
|
||||
return n > 0;
|
||||
}
|
||||
|
||||
const char *usbdesc_known_name(uint16_t vid, uint16_t pid, const char *manufacturer)
|
||||
{
|
||||
static const char adapter2[] = "8BitDo USB Wireless Adapter 2";
|
||||
int by_8bitdo = manufacturer && !strcmp(manufacturer, "8BitDo");
|
||||
|
||||
if (vid == VID_8BITDO && (pid == 0x3105 || pid == 0x3106 || pid == 0x3107)) return adapter2;
|
||||
if (vid == 0x045E && pid == 0x028E && by_8bitdo) return adapter2; /* its Xbox 360 identity */
|
||||
return NULL;
|
||||
}
|
||||
+13
-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 {
|
||||
@@ -19,17 +20,24 @@ typedef struct {
|
||||
uint8_t iface;
|
||||
uint8_t ep_in, ep_out; /* ep_out 0: none */
|
||||
uint16_t mps_in, mps_out;
|
||||
uint32_t hide; /* bit n: interface n is a boot keyboard or mouse beside the pad
|
||||
(the USB Wireless Adapter 2's macros), hidden from the console */
|
||||
} usbdesc_pick;
|
||||
|
||||
/* Picks the interface to read:
|
||||
* 1. an XInput interface (class ff, subclass 5d, protocol 01), any vendor;
|
||||
* 2. on an 8BitDo, the first HID interface that is not a boot keyboard or
|
||||
* 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. */
|
||||
int usbdesc_string(const unsigned char *d, int len, char *out, size_t size);
|
||||
|
||||
/* The product's name, where its own is not: the 8BitDo USB Wireless Adapter 2
|
||||
* calls itself "8BitDo Receiver" or "Controller". NULL if none is known. */
|
||||
const char *usbdesc_known_name(uint16_t vid, uint16_t pid, const char *manufacturer);
|
||||
|
||||
#endif
|
||||
+293
-17
@@ -1,9 +1,11 @@
|
||||
#include "usbpad.h"
|
||||
#include "log.h"
|
||||
#include "mapswitch.h"
|
||||
#include "usbdesc.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <dev/usb/usb.h>
|
||||
@@ -20,9 +22,23 @@
|
||||
#define EP_OUT N_RD /* transfer index of the OUT endpoint, if any */
|
||||
#define RD_MAX 64
|
||||
#define T_ALIVE 1000 /* ms between checks that the device is still there */
|
||||
#define N_BUS 4
|
||||
#define N_ADDR 16
|
||||
|
||||
/* Per /dev/ugenB.A: held by an open usbpad, or looked at and not a pad. A
|
||||
* node not a pad is not opened again until it changes (a device enumerating
|
||||
* anew gets a new node), so the scan can run often: the USB Wireless
|
||||
* 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 {
|
||||
int fd;
|
||||
int bus, addr;
|
||||
char path[24];
|
||||
char name[64];
|
||||
int kind; /* USBPAD_* (usbdesc.h) */
|
||||
@@ -35,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)
|
||||
@@ -43,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];
|
||||
@@ -55,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. */
|
||||
@@ -84,6 +105,40 @@ static int send_out(usbpad *u, const unsigned char *data, int n)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* A control transfer from the device; the bytes read, -1 if it failed. */
|
||||
static int ctrl_in(int fd, int type, int req, int value, int index, unsigned char *buf, int len)
|
||||
{
|
||||
struct usb_ctl_request r;
|
||||
|
||||
memset(&r, 0, sizeof r);
|
||||
r.ucr_data = buf;
|
||||
r.ucr_flags = 0x0004; /* USB_SHORT_XFER_OK (usbdi.h, kernel-only header) */
|
||||
r.ucr_request.bmRequestType = (uint8_t)type;
|
||||
r.ucr_request.bRequest = (uint8_t)req;
|
||||
USETW(r.ucr_request.wValue, value);
|
||||
USETW(r.ucr_request.wIndex, index);
|
||||
USETW(r.ucr_request.wLength, len);
|
||||
if (ioctl(fd, USB_DO_REQUEST, &r) != 0) return -1;
|
||||
return r.ucr_actlen;
|
||||
}
|
||||
|
||||
/* What Windows asks of an XInput device when it arrives: the Microsoft OS
|
||||
* string descriptor (0xEE) and the XUSB driver's vendor request. The USB
|
||||
* Wireless Adapter 2 picks its mode from the host it sees ("automatic mode
|
||||
* switch supports Windows and Switch"): answered like this, it stays in
|
||||
* XInput on its own. Pads that do not know them refuse, which is harmless. */
|
||||
static void xinput_windows_hello(usbpad *u)
|
||||
{
|
||||
unsigned char buf[64];
|
||||
int os, xusb;
|
||||
|
||||
memset(buf, 0, sizeof buf);
|
||||
os = ctrl_in(u->fd, UT_READ_DEVICE, UR_GET_DESCRIPTOR, 0x03EE, 0, buf, 18);
|
||||
memset(buf, 0, sizeof buf);
|
||||
xusb = ctrl_in(u->fd, 0xC1, 0x01, 0x0100, 0x0000, buf, 20);
|
||||
log_line("usb: %s: Windows hello: OS string %d, XUSB request %d", u->path, os, xusb);
|
||||
}
|
||||
|
||||
/* String descriptor `index` (US English), as ASCII. A standard GET_DESCRIPTOR. */
|
||||
static int read_string(int fd, int index, char *out, size_t size)
|
||||
{
|
||||
@@ -119,37 +174,150 @@ static int start_read(usbpad *u, int i)
|
||||
return 1;
|
||||
}
|
||||
|
||||
static usbpad *try_node(int bus, int addr)
|
||||
/* ---- 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)
|
||||
{
|
||||
static const char *kinds[] = { "", "8BitDo HID (DInput)", "XInput" };
|
||||
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", "Switch Pro" };
|
||||
struct usb_device_descriptor dd;
|
||||
struct usb_device_info di;
|
||||
struct usb_fs_init init;
|
||||
usbdesc_pick pick;
|
||||
char maker[32] = "";
|
||||
const char *known;
|
||||
usbpad *u;
|
||||
int i;
|
||||
|
||||
u = calloc(1, sizeof *u);
|
||||
if (!u) return NULL;
|
||||
u->bus = bus;
|
||||
u->addr = addr;
|
||||
snprintf(u->path, sizeof u->path, "/dev/ugen%d.%d", bus, addr);
|
||||
u->fd = open(u->path, O_RDWR);
|
||||
if (u->fd < 0) goto fail;
|
||||
if (ioctl(u->fd, USB_GET_DEVICE_DESC, &dd) != 0) goto fail;
|
||||
u->kind = choose_interface(u->fd, UGETW(dd.idVendor), &pick);
|
||||
if (u->kind == USBPAD_NONE) goto fail;
|
||||
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;
|
||||
}
|
||||
|
||||
/* The product name: USB_GET_DEVICEINFO fails on the 8BitDo dongle, the
|
||||
* string descriptor itself does not. */
|
||||
* string descriptor itself does not. A name known for its id wins. */
|
||||
memset(&di, 0, sizeof di);
|
||||
if (!read_string(u->fd, dd.iProduct, u->name, sizeof u->name)) {
|
||||
read_string(u->fd, dd.iManufacturer, maker, sizeof maker);
|
||||
if ((known = usbdesc_known_name(UGETW(dd.idVendor), UGETW(dd.idProduct), maker)))
|
||||
snprintf(u->name, sizeof u->name, "%s", known);
|
||||
else if (!read_string(u->fd, dd.iProduct, u->name, sizeof u->name)) {
|
||||
if (ioctl(u->fd, USB_GET_DEVICEINFO, &di) == 0 && di.udi_product[0])
|
||||
snprintf(u->name, sizeof u->name, "%.60s", di.udi_product);
|
||||
else
|
||||
@@ -198,13 +366,25 @@ static usbpad *try_node(int bus, int addr)
|
||||
}
|
||||
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. */
|
||||
for (i = 0; i < 32; i++)
|
||||
if (pick.hide & (1u << i)) {
|
||||
int n = i;
|
||||
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. */
|
||||
static const unsigned char led[3] = { 0x01, 0x03, 0x06 };
|
||||
log_line("usb: %s: XInput LED %s", u->path,
|
||||
!u->ep_out ? "not sent (no OUT endpoint)" : send_out(u, led, 3) ? "set" : "not acknowledged");
|
||||
}
|
||||
g_held[bus][addr] = 1;
|
||||
return u;
|
||||
|
||||
fail:
|
||||
@@ -215,16 +395,39 @@ fail:
|
||||
|
||||
usbpad *usbpad_open(void)
|
||||
{
|
||||
int bus, addr;
|
||||
long now = now_ms();
|
||||
int bus, addr, not_pad;
|
||||
|
||||
for (bus = 0; bus < 4; bus++)
|
||||
for (addr = 2; addr < 16; addr++) {
|
||||
for (bus = 0; bus < N_BUS; bus++)
|
||||
for (addr = 2; addr < N_ADDR; addr++) {
|
||||
char path[24];
|
||||
struct stat sb;
|
||||
usbpad *u;
|
||||
if (bus == 0 && addr == 2) continue; /* the internal Bluetooth chip: never touched */
|
||||
if (g_held[bus][addr]) continue;
|
||||
snprintf(path, sizeof path, "/dev/ugen%d.%d", bus, addr);
|
||||
if (access(path, F_OK) != 0) continue;
|
||||
if ((u = try_node(bus, addr))) return u;
|
||||
if (stat(path, &sb) != 0) {
|
||||
g_not_pad[bus][addr].set = 0;
|
||||
continue;
|
||||
}
|
||||
if (g_not_pad[bus][addr].set && g_not_pad[bus][addr].ino == sb.st_ino &&
|
||||
g_not_pad[bus][addr].ctime == (long)sb.st_ctime && now - g_not_pad[bus][addr].seen < T_NOT_PAD)
|
||||
continue; /* looked at a moment ago, unchanged */
|
||||
not_pad = 0;
|
||||
if ((u = try_node(bus, addr, ¬_pad))) {
|
||||
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;
|
||||
g_not_pad[bus][addr].seen = now;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
@@ -236,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)
|
||||
@@ -266,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");
|
||||
@@ -274,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) {
|
||||
@@ -292,6 +567,7 @@ void usbpad_close(usbpad *u)
|
||||
struct usb_fs_uninit un;
|
||||
|
||||
if (!u) return;
|
||||
if (u->bus >= 0 && u->bus < N_BUS && u->addr >= 0 && u->addr < N_ADDR) g_held[u->bus][u->addr] = 0;
|
||||
if (u->fd >= 0) {
|
||||
memset(&un, 0, sizeof un);
|
||||
ioctl(u->fd, USB_FS_UNINIT, &un);
|
||||
|
||||
+13
-3
@@ -10,14 +10,18 @@
|
||||
* 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. NULL if there is none right now. */
|
||||
/* Looks for a supported pad not open already. NULL if there is none right
|
||||
* now. Cheap enough to call every 100 ms: a node found not to be a pad is
|
||||
* not opened again for a while unless it changes. */
|
||||
usbpad *usbpad_open(void);
|
||||
|
||||
/* Takes what came in. Returns 1 with *st updated when a controller report
|
||||
@@ -25,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.0.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
|
||||
@@ -0,0 +1,243 @@
|
||||
#include "web.h"
|
||||
#include "log.h"
|
||||
#include "util.h"
|
||||
#include "version.h"
|
||||
#include "webpage.h"
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <sys/time.h>
|
||||
#include <sys/types.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define N_CLIENTS 8
|
||||
#define REQ_MAX 2048
|
||||
#define T_CLIENT 3000 /* ms a connection may take to send its request */
|
||||
|
||||
static int g_listen = -1;
|
||||
static struct { int fd; char buf[REQ_MAX]; int len; long t0; } g_cl[N_CLIENTS];
|
||||
|
||||
int web_start(int port)
|
||||
{
|
||||
struct sockaddr_in sin;
|
||||
int one = 1, i;
|
||||
|
||||
for (i = 0; i < N_CLIENTS; i++) g_cl[i].fd = -1;
|
||||
g_listen = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (g_listen < 0) {
|
||||
log_line("web: socket errno %d", errno);
|
||||
return 0;
|
||||
}
|
||||
setsockopt(g_listen, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
|
||||
memset(&sin, 0, sizeof sin);
|
||||
sin.sin_family = AF_INET;
|
||||
sin.sin_port = htons((uint16_t)port);
|
||||
sin.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
if (bind(g_listen, (struct sockaddr *)&sin, sizeof sin) != 0 || listen(g_listen, 8) != 0) {
|
||||
log_line("web: port %d: errno %d, no settings page", port, errno);
|
||||
close(g_listen);
|
||||
g_listen = -1;
|
||||
return 0;
|
||||
}
|
||||
fcntl(g_listen, F_SETFL, fcntl(g_listen, F_GETFL) | O_NONBLOCK);
|
||||
log_line("web: settings page on port %d", port);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void web_stop(void)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < N_CLIENTS; i++)
|
||||
if (g_cl[i].fd >= 0) {
|
||||
close(g_cl[i].fd);
|
||||
g_cl[i].fd = -1;
|
||||
}
|
||||
if (g_listen >= 0) close(g_listen);
|
||||
g_listen = -1;
|
||||
}
|
||||
|
||||
/* A JSON string, with what JSON escapes (control characters dropped). */
|
||||
static int json_str(char *out, int size, const char *s)
|
||||
{
|
||||
int n = 0;
|
||||
|
||||
if (size < 3) return 0;
|
||||
out[n++] = '"';
|
||||
for (; *s && n < size - 3; s++) {
|
||||
unsigned char c = (unsigned char)*s;
|
||||
if (c == '"' || c == '\\') {
|
||||
if (n >= size - 4) break;
|
||||
out[n++] = '\\';
|
||||
out[n++] = (char)c;
|
||||
} else if (c >= 0x20) {
|
||||
out[n++] = (char)c;
|
||||
}
|
||||
}
|
||||
out[n++] = '"';
|
||||
out[n] = 0;
|
||||
return n;
|
||||
}
|
||||
|
||||
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\":[", PADBRIDGE_VERSION);
|
||||
for (i = 0; i < n && len < size; i++) {
|
||||
const web_pad *p = &pads[i];
|
||||
char name[160], user[100];
|
||||
if (!p->present) continue;
|
||||
json_str(name, sizeof name, p->name);
|
||||
json_str(user, sizeof user, p->user);
|
||||
len += snprintf(out + len, (size_t)(size - len),
|
||||
"%s{\"slot\":%d,\"name\":%s,\"mode\":\"%s\",\"connected\":%d,\"user\":%s,\"swap\":%d,"
|
||||
"\"buttons\":%u,\"lx\":%u,\"ly\":%u,\"rx\":%u,\"ry\":%u,\"l2\":%u,\"r2\":%u}",
|
||||
first ? "" : ",", i, name, p->mode, p->connected ? 1 : 0, user, p->swap ? 1 : 0,
|
||||
(unsigned)p->st.buttons, p->st.lx, p->st.ly, p->st.rx, p->st.ry, p->st.l2, p->st.r2);
|
||||
first = 0;
|
||||
}
|
||||
if (len < size) len += snprintf(out + len, (size_t)(size - len), "]}");
|
||||
return len < size ? len : size - 1;
|
||||
}
|
||||
|
||||
static void send_all(int fd, const char *p, int n)
|
||||
{
|
||||
long t0 = now_ms();
|
||||
|
||||
while (n > 0 && now_ms() - t0 < 500) {
|
||||
ssize_t w = send(fd, p, (size_t)n, 0);
|
||||
if (w > 0) {
|
||||
p += w;
|
||||
n -= (int)w;
|
||||
} else if (w < 0 && (errno == EAGAIN || errno == EINTR)) {
|
||||
usleep(1000);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void respond(int fd, const char *status, const char *type, const char *body, int len)
|
||||
{
|
||||
char head[256];
|
||||
int h = snprintf(head, sizeof head,
|
||||
"HTTP/1.1 %s\r\nContent-Type: %s\r\nContent-Length: %d\r\n"
|
||||
"Cache-Control: no-store\r\nConnection: close\r\n\r\n", status, type, len);
|
||||
send_all(fd, head, h);
|
||||
send_all(fd, body, len);
|
||||
}
|
||||
|
||||
/* The value of `key` in a query string, -1 if absent. */
|
||||
static int query_int(const char *path, const char *key)
|
||||
{
|
||||
const char *q = strchr(path, '?');
|
||||
size_t k = strlen(key);
|
||||
|
||||
while (q) {
|
||||
q++;
|
||||
if (!strncmp(q, key, k) && q[k] == '=') return atoi(q + k + 1);
|
||||
q = strchr(q, '&');
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void handle(int fd, char *req, const web_pad *pads, int n, void (*set_swap)(int slot, int on))
|
||||
{
|
||||
char method[8], path[256];
|
||||
static char json[4096];
|
||||
|
||||
if (sscanf(req, "%7s %255s", method, path) != 2) {
|
||||
respond(fd, "400 Bad Request", "text/plain", "bad request\n", 12);
|
||||
return;
|
||||
}
|
||||
if (!strcmp(method, "GET") && (!strcmp(path, "/") || !strcmp(path, "/index.html"))) {
|
||||
respond(fd, "200 OK", "text/html; charset=utf-8", WEB_PAGE, (int)sizeof WEB_PAGE - 1);
|
||||
} else if (!strcmp(method, "GET") && !strncmp(path, "/api/status", 11)) {
|
||||
int len = web_status_json(pads, n, json, sizeof json);
|
||||
respond(fd, "200 OK", "application/json", json, len);
|
||||
} else if (!strcmp(method, "POST") && !strncmp(path, "/api/swap", 9)) {
|
||||
int slot = query_int(path, "slot"), on = query_int(path, "on");
|
||||
if (slot < 0 || slot >= n || !pads[slot].present || (on != 0 && on != 1)) {
|
||||
respond(fd, "400 Bad Request", "application/json", "{\"ok\":false}", 12);
|
||||
return;
|
||||
}
|
||||
set_swap(slot, on);
|
||||
respond(fd, "200 OK", "application/json", "{\"ok\":true}", 11);
|
||||
} else {
|
||||
respond(fd, "404 Not Found", "text/plain", "not found\n", 10);
|
||||
}
|
||||
}
|
||||
|
||||
void web_poll(const web_pad *pads, int n, void (*set_swap)(int slot, int on))
|
||||
{
|
||||
long now;
|
||||
int i;
|
||||
|
||||
if (g_listen < 0) return;
|
||||
now = now_ms();
|
||||
for (;;) { /* new connections */
|
||||
int fd = accept(g_listen, NULL, NULL), slot = -1;
|
||||
if (fd < 0) break;
|
||||
for (i = 0; i < N_CLIENTS; i++)
|
||||
if (g_cl[i].fd < 0) {
|
||||
slot = i;
|
||||
break;
|
||||
}
|
||||
if (slot < 0) {
|
||||
close(fd); /* too many at once */
|
||||
continue;
|
||||
}
|
||||
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
|
||||
g_cl[slot].fd = fd;
|
||||
g_cl[slot].len = 0;
|
||||
g_cl[slot].buf[0] = 0;
|
||||
g_cl[slot].t0 = now;
|
||||
}
|
||||
for (i = 0; i < N_CLIENTS; i++) { /* what they sent */
|
||||
ssize_t r;
|
||||
if (g_cl[i].fd < 0) continue;
|
||||
r = recv(g_cl[i].fd, g_cl[i].buf + g_cl[i].len, (size_t)(REQ_MAX - 1 - g_cl[i].len), 0);
|
||||
if (r > 0) {
|
||||
g_cl[i].len += (int)r;
|
||||
g_cl[i].buf[g_cl[i].len] = 0;
|
||||
}
|
||||
if (strstr(g_cl[i].buf, "\r\n\r\n") || g_cl[i].len >= REQ_MAX - 1) {
|
||||
handle(g_cl[i].fd, g_cl[i].buf, pads, n, set_swap);
|
||||
} else if (!(r == 0 || (r < 0 && errno != EAGAIN && errno != EINTR)) && now - g_cl[i].t0 < T_CLIENT) {
|
||||
continue; /* still coming */
|
||||
}
|
||||
close(g_cl[i].fd);
|
||||
g_cl[i].fd = -1;
|
||||
g_cl[i].len = 0;
|
||||
g_cl[i].buf[0] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int web_local_ip(char *out, int size)
|
||||
{
|
||||
struct sockaddr_in sin;
|
||||
socklen_t len = sizeof sin;
|
||||
int fd = socket(AF_INET, SOCK_DGRAM, 0), ok = 0;
|
||||
|
||||
out[0] = 0;
|
||||
if (fd < 0) return 0;
|
||||
/* A UDP "connection" only picks the route: nothing is sent. */
|
||||
memset(&sin, 0, sizeof sin);
|
||||
sin.sin_family = AF_INET;
|
||||
sin.sin_port = htons(53);
|
||||
sin.sin_addr.s_addr = htonl(0x08080808);
|
||||
if (connect(fd, (struct sockaddr *)&sin, sizeof sin) == 0 &&
|
||||
getsockname(fd, (struct sockaddr *)&sin, &len) == 0 && sin.sin_addr.s_addr != 0) {
|
||||
ok = inet_ntop(AF_INET, &sin.sin_addr, out, (socklen_t)size) != NULL;
|
||||
}
|
||||
close(fd);
|
||||
return ok;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/* padbridge's settings page, http://<console>:8090 from any browser on the local
|
||||
* network (phone, computer, the console's own).
|
||||
*
|
||||
* GET / the page
|
||||
* GET /api/status the pads now, as JSON (the page asks ~10 times a second)
|
||||
* POST /api/swap?slot=N&on=1 A/B and X/Y exchanged for the pad in slot N (0 to undo)
|
||||
*
|
||||
* Never blocks: web_poll() takes what has arrived and answers what is
|
||||
* complete, so a slow browser does not delay the pads. */
|
||||
#ifndef PADBRIDGE_WEB_H
|
||||
#define PADBRIDGE_WEB_H
|
||||
|
||||
#include "pad.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define WEB_PORT 8090
|
||||
|
||||
typedef struct {
|
||||
int present; /* a pad is open in this slot */
|
||||
int connected; /* its virtual DualSense exists */
|
||||
char name[64];
|
||||
char mode[8]; /* DInput, XInput */
|
||||
char user[40]; /* the console user it is given to, "" none */
|
||||
int swap;
|
||||
pad_state st;
|
||||
} web_pad;
|
||||
|
||||
int web_start(int port); /* 0 if it cannot listen (logged) */
|
||||
void web_poll(const web_pad *pads, int n, void (*set_swap)(int slot, int on));
|
||||
void web_stop(void);
|
||||
|
||||
/* The console's address on the local network, for the notification. 0 if unknown. */
|
||||
int web_local_ip(char *out, int size);
|
||||
|
||||
/* The JSON for /api/status, into out (size bytes). Its length. */
|
||||
int web_status_json(const web_pad *pads, int n, char *out, int size);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,73 @@
|
||||
/* 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 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>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}"
|
||||
"header{padding:18px 20px;border-bottom:1px solid var(--line)}h1{margin:0;font-size:20px}"
|
||||
"header small{color:var(--dim)}main{padding:16px;display:grid;gap:14px;"
|
||||
"grid-template-columns:repeat(auto-fill,minmax(320px,1fr))}"
|
||||
".card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:16px}"
|
||||
".card h2{margin:0 0 2px;font-size:16px}.meta{color:var(--dim);font-size:13px;margin-bottom:12px}"
|
||||
".dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--dim);margin-right:6px}"
|
||||
".dot.ok{background:var(--ok)}"
|
||||
"label.sw{display:flex;align-items:center;gap:10px;cursor:pointer;padding:10px 0;"
|
||||
"border-top:1px solid var(--line);border-bottom:1px solid var(--line);margin-bottom:12px}"
|
||||
"label.sw input{width:20px;height:20px}label.sw span small{display:block;color:var(--dim);font-size:12px}"
|
||||
".btns{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px}"
|
||||
".b{padding:4px 9px;border-radius:7px;border:1px solid var(--line);color:var(--dim);font-size:12px}"
|
||||
".b.p{background:var(--on);border-color:var(--on);color:#fff}"
|
||||
".sticks{display:flex;gap:18px;align-items:center}"
|
||||
".stick{width:70px;height:70px;border-radius:50%;border:1px solid var(--line);position:relative}"
|
||||
".stick i{position:absolute;width:12px;height:12px;border-radius:50%;background:var(--on);margin:-6px 0 0 -6px}"
|
||||
".trig{flex:1}.bar{height:8px;border-radius:4px;background:var(--line);margin:4px 0 10px;overflow:hidden}"
|
||||
".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>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/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],"
|
||||
"['Up',0x10],['Down',0x40],['Left',0x80],['Right',0x20]];"
|
||||
"var busy={};"
|
||||
"function esc(s){return String(s).replace(/[&<>\"']/g,function(c){return '&#'+c.charCodeAt(0)+';'})}"
|
||||
"function card(p){var h='<div class=card id=c'+p.slot+'><h2>'+esc(p.name)+'</h2><div class=meta>'"
|
||||
"+'<span class=\"dot'+(p.connected?' ok':'')+'\"></span>'+(p.connected?'Connected':'Waiting')"
|
||||
"+' · '+esc(p.mode)+' · slot '+(p.slot+1)+(p.user?' · '+esc(p.user):'')+'</div>'"
|
||||
"+'<label class=sw><input type=checkbox data-s='+p.slot+(p.swap?' checked':'')+' onchange=sw(this)>'"
|
||||
"+'<span>Nintendo layout<small>Swap A/B and X/Y, for a pad whose bottom button acts as Circle</small></span></label>'"
|
||||
"+'<div class=btns>';for(var i=0;i<B.length;i++)h+='<span class=b data-m='+B[i][1]+'>'+B[i][0]+'</span>';"
|
||||
"h+='</div><div class=sticks><div class=stick><i></i></div><div class=stick><i></i></div>'"
|
||||
"+'<div class=trig>L2<div class=bar><i></i></div>R2<div class=bar><i></i></div></div></div></div>';return h}"
|
||||
"function live(p){var c=document.getElementById('c'+p.slot);if(!c)return;"
|
||||
"var s=c.querySelectorAll('.b');for(var i=0;i<s.length;i++)"
|
||||
"s[i].className='b'+((p.buttons&+s[i].getAttribute('data-m'))?' p':'');"
|
||||
"var d=c.querySelectorAll('.stick i');d[0].style.left=(p.lx/255*100)+'%';d[0].style.top=(p.ly/255*100)+'%';"
|
||||
"d[1].style.left=(p.rx/255*100)+'%';d[1].style.top=(p.ry/255*100)+'%';"
|
||||
"var t=c.querySelectorAll('.bar i');t[0].style.width=(p.l2/255*100)+'%';t[1].style.width=(p.r2/255*100)+'%';"
|
||||
"var x=c.querySelector('input');if(!busy[p.slot])x.checked=!!p.swap}"
|
||||
"var shown='';"
|
||||
"function tick(){if(document.hidden)return setTimeout(tick,500);"
|
||||
"fetch('/api/status',{cache:'no-store'}).then(function(r){return r.json()}).then(function(j){"
|
||||
"document.getElementById('st').textContent='Version '+j.version+' running';"
|
||||
"var key=j.pads.map(function(p){return p.slot+p.name+p.mode+p.connected+p.user}).join('|');"
|
||||
"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='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)})}"
|
||||
"tick();"
|
||||
"</script></body></html>";
|
||||
|
||||
#endif
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/* The settings file and the A/B, X/Y swap. Runs on the PC. */
|
||||
#include "config.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)
|
||||
|
||||
int main(void)
|
||||
{
|
||||
static const char *path = "build/test_config.txt";
|
||||
pad_state st;
|
||||
FILE *f;
|
||||
|
||||
remove(path);
|
||||
config_load(path);
|
||||
CHECK(!config_swap("8BitDo USB Wireless Adapter 2"), "swap on with no file");
|
||||
|
||||
CHECK(config_set_swap("8BitDo USB Wireless Adapter 2", 1), "set");
|
||||
CHECK(config_swap("8BitDo USB Wireless Adapter 2"), "swap off after set");
|
||||
CHECK(!config_swap("8BitDo Ultimate 2C Wireless Controller"), "another pad swapped");
|
||||
|
||||
config_load(path); /* read back from the file */
|
||||
CHECK(config_swap("8BitDo USB Wireless Adapter 2"), "swap lost on reload");
|
||||
|
||||
CHECK(config_set_swap("8BitDo USB Wireless Adapter 2", 0), "unset");
|
||||
config_load(path);
|
||||
CHECK(!config_swap("8BitDo USB Wireless Adapter 2"), "swap kept after unset");
|
||||
|
||||
/* lines it does not know are kept, a damaged file is no harm */
|
||||
f = fopen(path, "w");
|
||||
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");
|
||||
config_set_swap("Other Pad", 1);
|
||||
config_load(path);
|
||||
CHECK(config_swap("Some Pad") && config_swap("Other Pad"), "a swap lost on save");
|
||||
|
||||
pad_state_reset(&st);
|
||||
st.buttons = PAD_CROSS | PAD_SQUARE | PAD_UP | PAD_L1;
|
||||
config_apply_swap(&st);
|
||||
CHECK(st.buttons == (PAD_CIRCLE | PAD_TRIANGLE | PAD_UP | PAD_L1), "swap: %#x", (unsigned)st.buttons);
|
||||
config_apply_swap(&st);
|
||||
CHECK(st.buttons == (PAD_CROSS | PAD_SQUARE | PAD_UP | PAD_L1), "swap twice: %#x", (unsigned)st.buttons);
|
||||
|
||||
remove(path);
|
||||
printf("%s (%d failure(s))\n", fails ? "FAILED" : "all passed", fails);
|
||||
return fails != 0;
|
||||
}
|
||||
@@ -48,6 +48,9 @@ int main(void)
|
||||
expect("L4", R(0x04, 0x00, 0x0f, 127, 127, 127, 127, 0, 0), PAD_TOUCHPAD);
|
||||
expect("R4", R(0x20, 0x00, 0x0f, 127, 127, 127, 127, 0, 0), PAD_TOUCHPAD);
|
||||
expect("LT half",R(0x00, 0x00, 0x0f, 127, 127, 127, 127, 0, 43), PAD_L2);
|
||||
/* The 8BitDo USB Wireless Adapter 2 in DInput mode: a DualSense's touchpad click */
|
||||
expect("touchpad", R(0x00, 0x00, 0x1f, 127, 127, 127, 127, 0, 0), PAD_TOUCHPAD);
|
||||
expect("touchpad+up", R(0x00, 0x00, 0x10, 127, 127, 127, 127, 0, 0), PAD_TOUCHPAD | PAD_UP);
|
||||
|
||||
usbpad_parse_8bitdo(R(0, 0, 0x0f, 126, 31, 224, 136, 7, 9), 10, &st);
|
||||
if (st.lx != 126 || st.ly != 31 || st.rx != 224 || st.ry != 136 || st.r2 != 7 || st.l2 != 9) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+29
-8
@@ -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,18 +41,38 @@ 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, 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, 0, &p);
|
||||
CHECK(p.hide == 0, "301c: hide %#x", (unsigned)p.hide);
|
||||
}
|
||||
|
||||
static void test_names(void)
|
||||
{
|
||||
const char *n;
|
||||
|
||||
n = usbdesc_known_name(0x2DC8, 0x3105, "8BitDo");
|
||||
CHECK(n && !strcmp(n, "8BitDo USB Wireless Adapter 2"), "3105: %s", n ? n : "(none)");
|
||||
n = usbdesc_known_name(0x2DC8, 0x3106, "8BitDo");
|
||||
CHECK(n && !strcmp(n, "8BitDo USB Wireless Adapter 2"), "3106: %s", n ? n : "(none)");
|
||||
n = usbdesc_known_name(0x045E, 0x028E, "8BitDo");
|
||||
CHECK(n && !strcmp(n, "8BitDo USB Wireless Adapter 2"), "028e from 8BitDo: %s", n ? n : "(none)");
|
||||
CHECK(!usbdesc_known_name(0x045E, 0x028E, "Microsoft"), "a real Xbox 360 pad renamed");
|
||||
CHECK(!usbdesc_known_name(0x2DC8, 0x301C, "8BitDo"), "the 2C dongle renamed");
|
||||
}
|
||||
|
||||
/* Xbox 360 report: 00 14, buttons, LT, RT, LX, LY, RX, RY (int16, y up) */
|
||||
@@ -150,6 +170,7 @@ int main(void)
|
||||
{
|
||||
test_string();
|
||||
test_choose();
|
||||
test_names();
|
||||
test_xinput();
|
||||
printf("%s (%d failure(s))\n", fails ? "FAILED" : "all passed", fails);
|
||||
return fails != 0;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/* The status JSON of the settings page. Runs on the PC. */
|
||||
#include "web.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)
|
||||
|
||||
int main(void)
|
||||
{
|
||||
web_pad p[2];
|
||||
char out[2048];
|
||||
|
||||
memset(p, 0, sizeof p);
|
||||
pad_state_reset(&p[0].st);
|
||||
pad_state_reset(&p[1].st);
|
||||
web_status_json(p, 2, out, sizeof out);
|
||||
CHECK(strstr(out, "\"pads\":[]}") != NULL, "no pads: %s", out);
|
||||
|
||||
p[1].present = 1;
|
||||
p[1].connected = 1;
|
||||
p[1].swap = 1;
|
||||
strcpy(p[1].name, "Pad \"X\" \\ 2");
|
||||
strcpy(p[1].mode, "XInput");
|
||||
strcpy(p[1].user, "Sub");
|
||||
p[1].st.buttons = 0x4000;
|
||||
web_status_json(p, 2, out, sizeof out);
|
||||
CHECK(strstr(out, "\"slot\":1,\"name\":\"Pad \\\"X\\\" \\\\ 2\"") != NULL, "escaping: %s", out);
|
||||
CHECK(strstr(out, "\"connected\":1,\"user\":\"Sub\",\"swap\":1,\"buttons\":16384") != NULL, "fields: %s", out);
|
||||
CHECK(strstr(out, "\"slot\":0") == NULL, "an empty slot listed: %s", out);
|
||||
CHECK(web_status_json(p, 2, out, 40) < 40 && strlen(out) < 40, "a small buffer overflowed");
|
||||
|
||||
printf("%s (%d failure(s))\n", fails ? "FAILED" : "all passed", fails);
|
||||
return fails != 0;
|
||||
}
|
||||
Reference in new issue
Block a user