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

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

Tested on the PS5: rest mode for about three minutes: "rest mode:
controllers let go", "awake: looking for controllers again", and the 2C
connected again on its own. A shorter rest, where the console froze
padbridge before it saw it, also came back.
2026-10-23 13:38:21 -03:00
Sub 41bece1b31 padbridge 1.3.0: rumble in PS4 games, Switch Pro Controller, View + Menu as the touchpad (#6)
* Rumble research: log what the console asks of each virtual pad

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Switch rumble: felt on third-party pads too

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

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

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

* README: the GameSir G8 through the adapter (its Switch mode, Nintendo layout, rumble)
2026-10-23 11:40:36 -03:00
Sub f29a1de877 padbridge 1.2.0: pad2c renamed, README rewritten (#5)
* padbridge 1.2.0: pad2c renamed, README rewritten

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

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

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

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

* README: no mention of the G8 on a cable

* README: no section on cables and ports
2026-10-22 18:25:37 -03:00
Sub 70f3419796 pad2c 1.1.0: 8BitDo USB Wireless Adapter 2 (closes #1), several controllers, settings page (#4)
* pad2c 1.1.0 (in progress): the 8BitDo USB Wireless Adapter 2, several pads

- Several pads at once, each its own virtual DualSense (up to 4), each
  slot remembering its own user (/data/pad2c/owner, owner.1, ...).
- New pads looked for every 100 ms; a node found not to be a pad is not
  opened again for 2 s unless it changes.
- XInput pads get what Windows asks first (the Microsoft OS string and
  the XUSB vendor request) before the LED: the USB Wireless Adapter 2
  then stays in XInput on its own, instead of ending in a DualShock 4
  mode that stops sending after a moment.
- A boot keyboard or mouse beside the pad (the adapter's macros, the
  2C receiver in XInput) is detached from the console's driver, so the
  console lists only the controller among its accessories.
- DInput: the adapter's touchpad bit (a DualSense's touchpad click).
- The adapter is named "8BitDo USB Wireless Adapter 2" (it calls itself
  "8BitDo Receiver" or "Controller").
- pad2c-probe: product and manufacturer strings, device release, 90 s.

Tested on a PS5 Slim, 13.60, adapter firmware 1.10: DualSense, Ultimate
2C and GameSir G8 through the adapter; DualSense on the adapter and the
2C on its own receiver at the same time.

* Settings page on port 8090; README and CHANGELOG for 1.1.0

- src/web.c: a small HTTP server in the main loop, never blocking the
  pads: GET / (the page), GET /api/status (JSON), POST /api/swap.
- src/webpage.h: one self-contained page: each controller's name, mode,
  slot and user, a Nintendo layout switch, and its buttons, sticks and
  triggers live (about 10 updates a second).
- src/config.c: /data/pad2c/config, "swap <controller name>" lines;
  unknown lines kept.
- The start notification gives the page's address.
- Tests: the settings file and the swap, the status JSON.

Tested on the PS5: the page from a computer, the live view, and the
swap on and off with a DualSense through the adapter.
2026-10-22 18:01:23 -03:00
Sub 2e243fa093 pad2c 1.0.0: Ultimate 2C dongle in XInput mode (fixes #2), signed notifications (#3)
* pad2c 0.2.2: Ultimate 2C dongle in XInput mode (fixes #2)

With the controller in XInput mode the receiver enumerates as 2dc8:310a
with an XInput interface (ff/5d/01), a boot keyboard and a vendor HID.
0.2.1 read the first HID interrupt IN endpoint: the keyboard.

- usbdesc.c picks the interface from the configuration descriptor:
  XInput first, from any vendor, then on an 8BitDo the first HID that
  is not a boot keyboard or mouse.
- mapxinput.c maps the Xbox 360 input report to the DualSense layout.
- usbpad.c opens the XInput OUT endpoint and sets the player LED, as a
  PC does.
- tests/test_usb.c: the 310a descriptor from the issue, the 301c one,
  and the XInput map.
- tools/setup-macos.sh: build on macOS with Homebrew LLVM.

Tested on a PS5 Slim, 13.60: DInput (301c), switch to XInput (310a),
and back, all buttons, sticks and triggers.

* Notifications: "pad2c by Sub", then the controller, its mode and what happened

Every notification now reads

    pad2c by Sub
    8BitDo Ultimate 2C Wireless Controller (XInput) connected

The name is read from the device's product string descriptor with a
standard GET_DESCRIPTOR (USB_GET_DEVICEINFO fails on the 8BitDo dongle).
The receiver names itself the same in DInput (301c) and XInput (310a),
so the mode is shown beside it.

- usbdesc_string(): UTF-16LE string descriptor to ASCII, with tests.
- usbpad_mode(): "DInput" or "XInput".
- All messages in English.

Tested on the PS5: connect, disconnect, XInput and back.

* Version 1.0.0; CODEOWNERS: every change needs the owner's review

The XInput fix and the signed notifications are released as 1.0.0.
.github/CODEOWNERS names @sub0x00001 for every file, so the main
ruleset's code owner review asks for the owner on every pull request.
2026-10-22 15:38:30 -03:00
sub0x00001 6de27e3aea README: screenshots of the controller connecting and disconnecting 2026-10-06 09:55:10 -03:00
sub0x00001 7f2f0822cf README: the controller is a cheap second controller 2026-10-06 09:42:13 -03:00
sub0x00001 a41f640b7b README: why this controller and why the dongle 2026-10-06 09:41:23 -03:00
sub0x00001 25138ce0ab README: controller link, which controller is supported, FAQ; issue template for controller reports 2026-10-06 09:39:40 -03:00
37 changed files with 2649 additions and 234 deletions

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
# Every change needs the review of the owner.
* @sub0x00001
@@ -0,0 +1,22 @@
---
name: Controller report
about: Tried padbridge with a controller (working or not), or want support for a new one
labels: controller
---
**Controller** (brand, model, connection: dongle / USB cable):
**Console** (model, e.g. CFI-2114; firmware; payload loader):
**What happened** (works / partly / not detected):
**padbridge log** (`/data/padbridge/padbridge.log`, the lines from the last start):
```
```
**For a controller that is not supported yet:** send `padbridge-probe.elf` with the controller plugged in, press every
button one at a time while it runs, and paste `/data/padbridge/probe.log` here.
```
```
+1 -2
View File
@@ -1,4 +1,3 @@
build/
dist/*.elf
!dist/.keep
dist/
release/
+64
View File
@@ -1,5 +1,69 @@
# 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
now picks the interface from the descriptor: XInput first, from any vendor, then on an 8BitDo the first HID that is
not a boot keyboard or mouse. XInput reports are mapped like the DualSense layout, and the player LED is set as a
PC does. L4/R4 send nothing in XInput mode.
- Notifications: every one starts with "pad2c by Sub", then what happened, all in English, e.g.
"8BitDo Ultimate 2C Wireless Controller (XInput) connected". The name is the controller's own, read from its USB
string descriptor (`USB_GET_DEVICEINFO` fails on the dongle), and the mode is DInput or XInput.
- `tools/setup-macos.sh`: builds on macOS with Homebrew LLVM.
## 0.2.1
- "Turn off" in the console's menu works: the console calls `sceMbusDisconnectDevice` on the pad, which does nothing
to a virtual one (it stayed on, and the menu then showed it as a generic "Accessories" device). pad2c now removes
+17 -7
View File
@@ -1,16 +1,26 @@
# 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
cc -std=c11 -Wall -Wextra -Werror -Isrc -o build/test_map src/map8bitdo.c tests/test_map.c
./build/test_map
cc -std=c11 -Wall -Wextra -Werror -Isrc -o build/test_usb src/usbdesc.c src/mapxinput.c tests/test_usb.c
./build/test_usb
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
@@ -19,15 +29,15 @@ endif
CFLAGS := -std=gnu11 -Wall -Wextra -O2 -Isrc
LDLIBS := -lScePad -lSceUserService -ldl
SRCS := src/main.c src/usbpad.c src/map8bitdo.c src/ps5_vpad.c src/ps5_power.c src/lock.c src/log.c src/util.c
SRCS := src/main.c src/usbpad.c src/usbdesc.c src/map8bitdo.c src/mapxinput.c src/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
+209 -50
View File
@@ -1,81 +1,235 @@
# pad2c
# padbridge
**Use an 8BitDo Ultimate 2C (2.4G dongle) as a second controller on a jailbroken PS5, next to your DualSense.**
**Use controllers the PS5 does not accept, on a jailbroken PS5, as extra DualSense controllers.**
pad2c is a payload for a jailbroken PS5. It reads the 8BitDo 2.4G dongle 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:
## Status
- 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.
| | |
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/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>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>
## Compatibility
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 |
|---|---|
| Tested on | PS5 Slim (CFI-2114), firmware **13.60**, elfldr 0.26, kstuff-lite 1.11, Payload Manager 0.5.2 |
| Controller | 8BitDo Ultimate 2C Wireless, 2.4G dongle (`2dc8:301c`) |
| 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 |
| 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 |
Other firmwares and console models are untested.
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.
Notes:
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 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.
### Vibration
## Button map
**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:
| 8BitDo | DualSense |
| Controller | Rumbles |
|---|---|
| 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 |
| Ultimate 2C on its dongle | in XInput mode (tested) |
| Through the USB Wireless Adapter 2, Switch-type controllers (GameSir G8 tested, Switch Pro, Joy-Con) | with the adapter in its **Switch mode** (tested) |
| Through the USB Wireless Adapter 2, PlayStation controllers (DualSense tested) | in XInput mode |
**PS5 games do not rumble**: they send DualSense haptics, and the console turns even their classic rumble requests
into haptic audio before the virtual DualSense sees them. Controllers in DInput mode do not rumble either.
### Not yet
Vibration 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://&lt;console IP&gt;:8090** in a browser on the same network (phone, computer, the console's own); the
address is in the notification when padbridge starts. For each controller:
- its name, mode (DInput or XInput), slot and the user it is given to;
- **Nintendo layout**: A/B and X/Y swapped, applied at once and kept, for controllers that send the Nintendo layout
(A on the right), such as the GameSir G8 in Switch mode;
- its buttons, sticks and triggers, live, to check a mapping.
The page is open to the local network, without a password. Through the adapter, a setting belongs to the adapter:
it holds for whatever controller is paired with it.
## The 8BitDo USB Wireless Adapter 2
The adapter shows a different USB identity for each mode and picks its mode from the host it sees. padbridge answers
it as Windows does, so it settles in **XInput** by itself, and hides the keyboard and mouse the adapter adds in that
mode, so the console's Accessories list shows only the controller.
| Mode | How | Notes |
|---|---|---|
| XInput (`2dc8:3106`) | automatic | vibration in PS4 games for PlayStation controllers (DualSense tested); View + Menu as the touchpad |
| DInput (`2dc8:3105`) | hold **View + Left** (Create + Left on a DualSense) for 3 s; the light blinks twice | passes a DualSense's touchpad click |
| Switch (`057e:2009`, a Switch Pro Controller) | hold **- + L** (View + LB) for 3 s; the light blinks twice | **vibration for Switch-type controllers** (the adapter passes it on only in this mode); Capture as the touchpad; buttons by position, so a pad that sends them by label (GameSir G8) needs Nintendo layout |
| back to automatic | hold the adapter's button until its light goes off, then pair the controller again | |
| DualShock 4 (`054c:05c4`) | where it ends on a PS5 without padbridge's answer | does not work: it stops sending after a moment |
Pairing a DualSense with the adapter unpairs it from the console: plug it into the console with its cable and press
PS to pair it back. The adapter takes one controller at a time.
## The GameSir G8 Plus Galileo through the adapter
The G8's Bluetooth modes are Android, Switch and G-Touch; the USB Wireless Adapter 2 accepts only its **Switch**
mode. It then works in both of the adapter's modes, with two things to know:
| Adapter mode | Buttons | Vibration |
|---|---|---|
| **Switch** (hold **- + L** for 3 s) | tick **Nintendo layout** on the "Pro Controller" card of the settings page | ✅ (PS4 games) |
| XInput (automatic) | tick **Nintendo layout** on the "8BitDo USB Wireless Adapter 2" card | ❌ the adapter passes rumble in XInput to PlayStation controllers only |
- **Nintendo layout:** in Switch mode the G8 sends its buttons by their printed letter (its bottom button is "A",
which a Switch reads as the right one), not by position. A genuine Switch Pro Controller needs no setting.
- **Weak rumble:** simple rumble motors like the G8's do not move below a certain strength, and the Switch format
splits rumble into a low and a high band, of which such pads feel mostly the low one. In the Switch mode,
padbridge sends the stronger motor to the low band and starts any rumble at about 40%, so a light request (Crash
Bandicoot's spin asks 25%) is still felt.
- The setting is kept per card, so the G8 and a DualSense can share the adapter: the DualSense uses the adapter's
XInput mode, without Nintendo layout.
## The Ultimate 2C's two dongle modes
The controller keeps the last mode chosen. With the controller on:
- **DInput** (the default): hold **B + Home** until it switches off, then **B + Home** again until it switches on.
- **XInput**: the same with **X + Home**.
## Button map
| Controller | DualSense |
|---|---|
| A / B / X / Y (Xbox layout) | ✕ / ○ / □ / △ (by position) |
| LB / RB / LT / RT | L1 / R1 / L2 / R2 (analog) |
| View / Menu (Back / Start, - / +) | Create / Options |
| Home / Guide | PS |
| L3 / R3 | L3 / R3 |
| Ultimate 2C L4 / R4 (DInput), DualSense touchpad (adapter, DInput) | touchpad click |
| View + Menu pressed together, on XInput controllers | touchpad click |
| Switch Pro (and the adapter's Switch mode): B / A / Y / X, L / R, ZL / ZR, - / +, Home, Capture | ✕ / ○ / □ / △ (by position), L1 / R1, L2 / R2, Create / Options, PS, touchpad click |
## Notes
- "Turn off" in the console's menu disconnects the controller from the console, as with a DualSense, and Home
connects it again. The controller itself stays on until it goes to sleep by itself.
- Every notification reads "padbridge by Sub", then the controller, its mode and what happened.
- On XInput controllers, View or Menu pressed alone reaches the console 60 ms late: padbridge waits that long for
the other one, so that View + Menu (the touchpad) does not open the Create menu on the way.
## Files
All in `/data/padbridge/` on the console:
| File | |
|---|---|
| `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
and sends report 1. `src/map8bitdo.c` turns that report into DualSense buttons (`make test` checks it against
reports captured on a console). When the controller sleeps, the dongle enumerates again and the open device ends.
- `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
Needs [ps5-payload-sdk](https://github.com/ps5-payload-dev/sdk) and clang, lld and llvm (Linux or WSL):
Needs [ps5-payload-sdk](https://github.com/ps5-payload-dev/sdk) and clang, lld and llvm. `tools/setup-wsl.sh`
(Linux or WSL) or `tools/setup-macos.sh` (Homebrew) installs them and writes `~/.ps5sdk`:
```
export PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk
make test # the button map, on the PC
make # dist/pad2c-<version>.elf and dist/pad2c-probe.elf
. ~/.ps5sdk
make test # button maps, interface choice, settings, status JSON, on the PC
make # dist/padbridge-<version>.elf and dist/padbridge-probe.elf
```
## Credits
@@ -83,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.

After

Width:  |  Height:  |  Size: 149 KiB

+53
View File
@@ -0,0 +1,53 @@
#include "combo.h"
#define VM (PAD_CREATE | PAD_OPTIONS)
void combo_reset(combo *c)
{
c->since = -1;
c->held = 0;
c->tap_until = 0;
c->tap = 0;
c->on = 0;
}
uint32_t combo_filter(combo *c, uint32_t buttons, long now)
{
uint32_t vm = buttons & VM, rest = buttons & ~(uint32_t)VM;
if (c->on) {
if (!vm) c->on = 0;
else return rest | PAD_TOUCHPAD;
}
if (vm == VM && c->since >= 0 && now - c->since < COMBO_WINDOW_MS) {
c->on = 1; /* the second one came in time */
c->since = -1;
c->held = 0;
return rest | PAD_TOUCHPAD;
}
if (vm == VM && c->since < 0 && !c->held && !c->tap) {
c->on = 1; /* both in the same report */
return rest | PAD_TOUCHPAD;
}
if (vm && vm != VM) {
if (c->since < 0 && !c->held) {
c->since = now;
c->held = vm;
}
if (c->held && now - c->since < COMBO_WINDOW_MS) return rest; /* held back */
c->held = 0; /* held past the window: as it is */
return buttons;
}
if (!vm && c->held) { /* a tap shorter than the window */
c->tap = c->held;
c->tap_until = now + COMBO_TAP_MS;
c->held = 0;
c->since = -1;
}
if (!vm) c->since = -1;
if (c->tap) {
if (now < c->tap_until) return rest | c->tap;
c->tap = 0;
}
return buttons;
}
+31
View File
@@ -0,0 +1,31 @@
/* View + Menu (Create + Options) pressed together as the touchpad click, for
* pads that have no touchpad and no spare button to stand for it: XInput
* pads (the Ultimate 2C in XInput mode sends no L4/R4; Star and Mapping never
* reach the console in either mode).
*
* Nobody presses two buttons in the same millisecond: View or Menu alone is
* held back for COMBO_WINDOW_MS, so that the console does not see a Create
* press on the way to the touchpad (it would open the Create menu). Held
* longer, or with the other one late, they go through as they are; a tap
* shorter than the window is sent when it ends, for COMBO_TAP_MS. Once the
* touchpad is on, it stays until both are released. */
#ifndef PADBRIDGE_COMBO_H
#define PADBRIDGE_COMBO_H
#include "pad.h"
#define COMBO_WINDOW_MS 60
#define COMBO_TAP_MS 60
typedef struct {
long since; /* ms: View or Menu alone since then, -1 if not */
uint32_t held; /* which of them is being held back */
long tap_until; /* ms: a short tap being sent until then */
uint32_t tap;
int on; /* the touchpad is on */
} combo;
void combo_reset(combo *c);
uint32_t combo_filter(combo *c, uint32_t buttons, long now);
#endif
+84
View File
@@ -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;
}
+23
View File
@@ -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
View File
@@ -1,6 +1,6 @@
/* The part of AnyPad PS5's host.h that ps5_vpad.c needs. */
#ifndef PAD2C_HOST_H
#define PAD2C_HOST_H
#ifndef PADBRIDGE_HOST_H
#define PADBRIDGE_HOST_H
#include "pad.h"
+5
View File
@@ -53,6 +53,11 @@ static int lock_is_live(const char *path, long boot)
return kill((pid_t)pid, 0) == 0 || errno == EPERM;
}
int lock_held(const char *path)
{
return lock_is_live(path, lock_boot_time());
}
int lock_take(const char *path)
{
char buf[64];
+1
View File
@@ -11,6 +11,7 @@
/* Returns 1 with the lock held, 0 if another copy is running or the file
* cannot be created. */
int lock_take(const char *path);
int lock_held(const char *path); /* 1 if a running copy holds the lock at path */
void lock_release(void);
/* The boot time of this machine, in seconds since 1970; 0 if unknown. */
+290 -77
View File
@@ -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,27 +60,227 @@ 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 */
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("pad2c is already running");
notify("Already running");
log_close();
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);
@@ -77,79 +288,81 @@ int main(void)
if (!vpad_init()) {
log_line("virtual pads not available; stopping");
notify("pad2c %s: cannot create a virtual DualSense (see %s)", PAD2C_VERSION, LOG_PATH);
notify("Cannot create a virtual DualSense\nSee %s", LOG_PATH);
lock_release();
log_close();
return 1;
}
notify("pad2c %s running\nSwitch the 8BitDo on (2.4G mode) with its dongle plugged in", PAD2C_VERSION);
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("pad2c: 8BitDo disconnected");
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);
log_line("controller connected (%s), virtual pad %s", usbpad_name(pad),
vpad_made ? "requested" : "refused");
notify("pad2c: 8BitDo connected");
}
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("pad2c: 8BitDo turned off\nPress Home on the controller to turn it back on");
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) {
@@ -159,17 +372,17 @@ 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();
log_line("stopped");
log_close();
notify("pad2c stopped (%s)", why);
notify("Stopped (%s)", why);
return 0;
}
+3 -1
View File
@@ -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
View File
@@ -0,0 +1,157 @@
#include "mapswitch.h"
#include <stddef.h>
#include <string.h>
void switch_sticks_reset(switch_sticks *c)
{
int i;
c->set = 0;
for (i = 0; i < 4; i++) {
c->centre[i] = 2048;
c->reach[i] = SWITCH_STICK_REACH;
}
}
/* A 12-bit stick value to 0..255, 128 at the centre. `up` flips it (the
* DualSense's y grows downwards). */
static uint8_t axis(switch_sticks *c, int i, int v, int up)
{
int d = up ? c->centre[i] - v : v - c->centre[i], a = d < 0 ? -d : d, out;
if (a > c->reach[i]) c->reach[i] = a;
out = d < 0 ? 128 - (a * 128 + c->reach[i] / 2) / c->reach[i] : 128 + (a * 127 + c->reach[i] / 2) / c->reach[i];
return (uint8_t)(out < 0 ? 0 : out > 255 ? 255 : out);
}
int switch_parse(const unsigned char *r, int n, switch_sticks *cal, pad_state *st)
{
static const struct { int byte; uint8_t bit; uint32_t pad; } map[] = {
{ 3, 0x01, PAD_SQUARE }, { 3, 0x02, PAD_TRIANGLE }, { 3, 0x04, PAD_CROSS }, { 3, 0x08, PAD_CIRCLE },
{ 3, 0x40, PAD_R1 }, { 3, 0x80, PAD_R2 }, { 4, 0x01, PAD_CREATE }, { 4, 0x02, PAD_OPTIONS },
{ 4, 0x04, PAD_R3 }, { 4, 0x08, PAD_L3 }, { 4, 0x10, PAD_PS }, { 4, 0x20, PAD_TOUCHPAD },
{ 5, 0x01, PAD_DOWN }, { 5, 0x02, PAD_UP }, { 5, 0x04, PAD_RIGHT }, { 5, 0x08, PAD_LEFT },
{ 5, 0x40, PAD_L1 }, { 5, 0x80, PAD_L2 },
};
int v[4];
size_t i;
if (n < 12 || (r[0] != 0x30 && r[0] != 0x21)) return 0;
v[0] = r[6] | (r[7] & 0x0F) << 8;
v[1] = r[7] >> 4 | r[8] << 4;
v[2] = r[9] | (r[10] & 0x0F) << 8;
v[3] = r[10] >> 4 | r[11] << 4;
if (!cal->set) {
for (i = 0; i < 4; i++) cal->centre[i] = v[i];
cal->set = 1;
}
pad_state_reset(st);
for (i = 0; i < sizeof map / sizeof map[0]; i++)
if (r[map[i].byte] & map[i].bit) st->buttons |= map[i].pad;
st->l2 = st->buttons & PAD_L2 ? 255 : 0; /* ZL and ZR are digital */
st->r2 = st->buttons & PAD_R2 ? 255 : 0;
st->lx = axis(cal, 0, v[0], 0);
st->ly = axis(cal, 1, v[1], 1);
st->rx = axis(cal, 2, v[2], 0);
st->ry = axis(cal, 3, v[3], 1);
return 1;
}
/* SDL's tables (SDL_hidapi_switch.c, zlib licence), after dekuNukem's
* rumble_data_table.md: an amplitude 0..65535 to the high band's byte and
* the low band's 9-bit value. */
static uint8_t high_amp(uint16_t a)
{
static const uint16_t t[101][2] = {
{ 0, 0x0 }, { 514, 0x2 }, { 775, 0x4 }, { 921, 0x6 }, { 1096, 0x8 }, { 1303, 0x0a }, { 1550, 0x0c },
{ 1843, 0x0e }, { 2192, 0x10 }, { 2606, 0x12 }, { 3100, 0x14 }, { 3686, 0x16 }, { 4383, 0x18 },
{ 5213, 0x1a }, { 6199, 0x1c }, { 7372, 0x1e }, { 7698, 0x20 }, { 8039, 0x22 }, { 8395, 0x24 },
{ 8767, 0x26 }, { 9155, 0x28 }, { 9560, 0x2a }, { 9984, 0x2c }, { 10426, 0x2e }, { 10887, 0x30 },
{ 11369, 0x32 }, { 11873, 0x34 }, { 12398, 0x36 }, { 12947, 0x38 }, { 13520, 0x3a }, { 14119, 0x3c },
{ 14744, 0x3e }, { 15067, 0x40 }, { 15397, 0x42 }, { 15734, 0x44 }, { 16079, 0x46 }, { 16431, 0x48 },
{ 16790, 0x4a }, { 17158, 0x4c }, { 17534, 0x4e }, { 17918, 0x50 }, { 18310, 0x52 }, { 18711, 0x54 },
{ 19121, 0x56 }, { 19540, 0x58 }, { 19967, 0x5a }, { 20405, 0x5c }, { 20851, 0x5e }, { 21308, 0x60 },
{ 21775, 0x62 }, { 22251, 0x64 }, { 22739, 0x66 }, { 23236, 0x68 }, { 23745, 0x6a }, { 24265, 0x6c },
{ 24797, 0x6e }, { 25340, 0x70 }, { 25894, 0x72 }, { 26462, 0x74 }, { 27041, 0x76 }, { 27633, 0x78 },
{ 28238, 0x7a }, { 28856, 0x7c }, { 29488, 0x7e }, { 30134, 0x80 }, { 30794, 0x82 }, { 31468, 0x84 },
{ 32157, 0x86 }, { 32861, 0x88 }, { 33581, 0x8a }, { 34316, 0x8c }, { 35068, 0x8e }, { 35836, 0x90 },
{ 36620, 0x92 }, { 37422, 0x94 }, { 38242, 0x96 }, { 39079, 0x98 }, { 39935, 0x9a }, { 40809, 0x9c },
{ 41703, 0x9e }, { 42616, 0xa0 }, { 43549, 0xa2 }, { 44503, 0xa4 }, { 45477, 0xa6 }, { 46473, 0xa8 },
{ 47491, 0xaa }, { 48531, 0xac }, { 49593, 0xae }, { 50679, 0xb0 }, { 51789, 0xb2 }, { 52923, 0xb4 },
{ 54082, 0xb6 }, { 55266, 0xb8 }, { 56476, 0xba }, { 57713, 0xbc }, { 58977, 0xbe }, { 60268, 0xc0 },
{ 61588, 0xc2 }, { 62936, 0xc4 }, { 64315, 0xc6 }, { 65535, 0xc8 },
};
int i;
for (i = 0; i < 101; i++)
if (a <= t[i][0]) return (uint8_t)t[i][1];
return (uint8_t)t[100][1];
}
static uint16_t low_amp(uint16_t a)
{
static const uint16_t t[101][2] = {
{ 0, 0x0040 }, { 514, 0x8040 }, { 775, 0x0041 }, { 921, 0x8041 }, { 1096, 0x0042 }, { 1303, 0x8042 },
{ 1550, 0x0043 }, { 1843, 0x8043 }, { 2192, 0x0044 }, { 2606, 0x8044 }, { 3100, 0x0045 },
{ 3686, 0x8045 }, { 4383, 0x0046 }, { 5213, 0x8046 }, { 6199, 0x0047 }, { 7372, 0x8047 },
{ 7698, 0x0048 }, { 8039, 0x8048 }, { 8395, 0x0049 }, { 8767, 0x8049 }, { 9155, 0x004a },
{ 9560, 0x804a }, { 9984, 0x004b }, { 10426, 0x804b }, { 10887, 0x004c }, { 11369, 0x804c },
{ 11873, 0x004d }, { 12398, 0x804d }, { 12947, 0x004e }, { 13520, 0x804e }, { 14119, 0x004f },
{ 14744, 0x804f }, { 15067, 0x0050 }, { 15397, 0x8050 }, { 15734, 0x0051 }, { 16079, 0x8051 },
{ 16431, 0x0052 }, { 16790, 0x8052 }, { 17158, 0x0053 }, { 17534, 0x8053 }, { 17918, 0x0054 },
{ 18310, 0x8054 }, { 18711, 0x0055 }, { 19121, 0x8055 }, { 19540, 0x0056 }, { 19967, 0x8056 },
{ 20405, 0x0057 }, { 20851, 0x8057 }, { 21308, 0x0058 }, { 21775, 0x8058 }, { 22251, 0x0059 },
{ 22739, 0x8059 }, { 23236, 0x005a }, { 23745, 0x805a }, { 24265, 0x005b }, { 24797, 0x805b },
{ 25340, 0x005c }, { 25894, 0x805c }, { 26462, 0x005d }, { 27041, 0x805d }, { 27633, 0x005e },
{ 28238, 0x805e }, { 28856, 0x005f }, { 29488, 0x805f }, { 30134, 0x0060 }, { 30794, 0x8060 },
{ 31468, 0x0061 }, { 32157, 0x8061 }, { 32861, 0x0062 }, { 33581, 0x8062 }, { 34316, 0x0063 },
{ 35068, 0x8063 }, { 35836, 0x0064 }, { 36620, 0x8064 }, { 37422, 0x0065 }, { 38242, 0x8065 },
{ 39079, 0x0066 }, { 39935, 0x8066 }, { 40809, 0x0067 }, { 41703, 0x8067 }, { 42616, 0x0068 },
{ 43549, 0x8068 }, { 44503, 0x0069 }, { 45477, 0x8069 }, { 46473, 0x006a }, { 47491, 0x806a },
{ 48531, 0x006b }, { 49593, 0x806b }, { 50679, 0x006c }, { 51789, 0x806c }, { 52923, 0x006d },
{ 54082, 0x806d }, { 55266, 0x006e }, { 56476, 0x806e }, { 57713, 0x006f }, { 58977, 0x806f },
{ 60268, 0x0070 }, { 61588, 0x8070 }, { 62936, 0x0071 }, { 64315, 0x8071 }, { 65535, 0x0072 },
};
int i;
for (i = 0; i < 101; i++)
if (a <= t[i][0]) return t[i][1];
return t[100][1];
}
uint8_t switch_rumble_boost(uint8_t v)
{
return v ? (uint8_t)(SWITCH_RUMBLE_FLOOR + (v * (255 - SWITCH_RUMBLE_FLOOR) + 127) / 255) : 0;
}
void switch_rumble_encode(uint8_t large, uint8_t small, unsigned char out[8])
{
static const unsigned char neutral[4] = { 0x00, 0x01, 0x40, 0x40 };
/* SDL's fixed frequencies: the high band 0x0074, the low band 0x3d */
const uint16_t hf = 0x0074;
const uint8_t lf = 0x3D;
uint8_t ha;
/* The low band takes the stronger of the two motors, not the large one
* alone: third-party pads (the GameSir G8 through the USB Wireless
* Adapter 2) hardly feel the high band, and some games rumble with the
* small motor only (Crash Bandicoot's spin: 0/63). */
uint16_t la;
unsigned char d[4];
if (!large && !small) {
memcpy(out, neutral, 4);
memcpy(out + 4, neutral, 4);
return;
}
large = switch_rumble_boost(large);
small = switch_rumble_boost(small);
ha = high_amp((uint16_t)(small * 257));
la = low_amp((uint16_t)((large > small ? large : small) * 257));
d[0] = (unsigned char)(hf & 0xFF);
d[1] = (unsigned char)(ha | ((hf >> 8) & 0x01));
d[2] = (unsigned char)(lf | ((la >> 8) & 0x80));
d[3] = (unsigned char)(la & 0xFF);
memcpy(out, d, 4);
memcpy(out + 4, d, 4);
}
+47
View File
@@ -0,0 +1,47 @@
/* The Nintendo Switch Pro Controller over USB (057e:2009): what the 8BitDo USB
* Wireless Adapter 2 shows in its Switch mode, and controllers wired in their
* Switch mode. Protocol as documented by dekuNukem's Nintendo Switch reverse
* engineering notes and implemented by SDL (zlib licence) and Linux's
* hid-nintendo.
*
* Report 0x30 (and 0x21, a subcommand's reply, which starts the same):
* [3] Y X B A SR SL R ZR [4] - + RS LS Home Capture [5] down up right left SR SL L ZL
* [6..8] left stick, [9..11] right stick: two 12-bit values each, y grows upwards
* Placed by position, as on a DualSense: B cross, A circle, Y square, X triangle;
* Capture is the touchpad. */
#ifndef PADBRIDGE_MAPSWITCH_H
#define PADBRIDGE_MAPSWITCH_H
#include "pad.h"
#include <stdint.h>
/* Stick calibration, learnt as the pad is used: the centre from the first
* report (sticks at rest), the reach from the furthest seen, at least
* SWITCH_STICK_REACH. */
#define SWITCH_STICK_REACH 1500
typedef struct {
int set;
int centre[4], reach[4]; /* lx ly rx ry */
} switch_sticks;
void switch_sticks_reset(switch_sticks *c);
/* Report 0x30 or 0x21 to a DualSense state. 0 if it is neither. */
int switch_parse(const unsigned char *r, int n, switch_sticks *cal, pad_state *st);
/* The 8 rumble bytes (left and right, the same) for a large and a small motor
* strength, 0..255: SDL's encoding and tables. The high band follows the small
* motor; the low band the stronger of the two (third-party pads hardly feel
* the high band). */
void switch_rumble_encode(uint8_t large, uint8_t small, unsigned char out[8]);
/* A motor strength lifted over the threshold of simple rumble motors: 0 stays
* 0, anything else goes from SWITCH_RUMBLE_FLOOR to 255. A weak request (Crash
* Bandicoot's spin asks 63) is otherwise not felt at all on a third-party pad
* through the adapter. */
#define SWITCH_RUMBLE_FLOOR 100
uint8_t switch_rumble_boost(uint8_t v);
#endif
+40
View File
@@ -0,0 +1,40 @@
#include "usbpad.h"
#include <stddef.h>
#include <stdint.h>
/* XInput (Xbox 360 wired protocol), input report:
* [0] 00 (type) [1] 14 (length)
* [2..3] buttons: up down left right Start Back L3 R3, LB RB Guide - A B X Y
* [4] LT [5] RT (0..255)
* [6..13] LX LY RX RY, int16 little-endian, centred on 0, y grows upwards
* Placed by position, as on a DualSense: A cross, B circle, X square, Y triangle.
* The Ultimate 2C sends nothing for L4/R4 in this mode. */
int usbpad_parse_xinput(const unsigned char *r, int n, pad_state *st)
{
static const struct { uint16_t bit; uint32_t pad; } map[] = {
{ 1u << 0, PAD_UP }, { 1u << 1, PAD_DOWN }, { 1u << 2, PAD_LEFT },
{ 1u << 3, PAD_RIGHT }, { 1u << 4, PAD_OPTIONS }, { 1u << 5, PAD_CREATE },
{ 1u << 6, PAD_L3 }, { 1u << 7, PAD_R3 }, { 1u << 8, PAD_L1 },
{ 1u << 9, PAD_R1 }, { 1u << 10, PAD_PS }, { 1u << 12, PAD_CROSS },
{ 1u << 13, PAD_CIRCLE }, { 1u << 14, PAD_SQUARE }, { 1u << 15, PAD_TRIANGLE },
};
unsigned b;
size_t i;
if (n < 14 || r[0] != 0x00 || r[1] < 0x14) return 0;
pad_state_reset(st);
b = (unsigned)r[2] | (unsigned)r[3] << 8;
for (i = 0; i < sizeof map / sizeof map[0]; i++)
if (b & map[i].bit) st->buttons |= map[i].pad;
st->l2 = r[4];
st->r2 = r[5];
/* -32768..32767 to 0..255; y flipped, as the DualSense grows downwards */
st->lx = (uint8_t)(((int16_t)(r[6] | r[7] << 8) + 32768) >> 8);
st->ly = (uint8_t)((32767 - (int16_t)(r[8] | r[9] << 8)) >> 8);
st->rx = (uint8_t)(((int16_t)(r[10] | r[11] << 8) + 32768) >> 8);
st->ry = (uint8_t)((32767 - (int16_t)(r[12] | r[13] << 8)) >> 8);
if (st->l2 > 24) st->buttons |= PAD_L2;
if (st->r2 > 24) st->buttons |= PAD_R2;
return 1;
}
+47 -12
View File
@@ -1,4 +1,4 @@
/* pad2c probe -- read-only look at the console's USB devices.
/* padbridge probe -- read-only look at the console's USB devices.
*
* For every /dev/ugenB.A that is not a hub and not the internal Bluetooth
* chip (ugen0.2, left alone on purpose): device descriptor, name, full
@@ -7,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;
}
+78 -25
View File
@@ -2,6 +2,7 @@
#include "host.h"
#include "log.h"
#include "util.h"
#include "version.h"
#include <sys/socket.h>
#include <sys/types.h>
@@ -68,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);
@@ -76,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). */
@@ -109,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);
}
@@ -177,12 +194,15 @@ static size_t g_line_len;
void notify(const char *fmt, ...)
{
/* Every notification: "padbridge by <author>", then what happened. */
static const char head[] = "padbridge by " PADBRIDGE_AUTHOR "\n";
notify_request req;
va_list ap;
memset(&req, 0, sizeof req);
memcpy(req.message, head, sizeof head);
va_start(ap, fmt);
vsnprintf(req.message, sizeof req.message, fmt, ap);
vsnprintf(req.message + sizeof head - 1, sizeof req.message - (sizeof head - 1), fmt, ap);
va_end(ap);
sceKernelSendNotificationRequest(0, &req, sizeof req, 0);
}
@@ -263,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)
@@ -277,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. */
@@ -377,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';
@@ -401,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);
@@ -519,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;
@@ -613,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,
@@ -640,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;
@@ -701,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
View File
@@ -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;
+114
View File
@@ -0,0 +1,114 @@
#include "usbdesc.h"
#include <string.h>
#define DT_INTERFACE 0x04
#define DT_ENDPOINT 0x05
typedef struct {
uint8_t num, cls, sub, proto;
uint8_t ep_in, ep_out;
uint16_t mps_in, mps_out;
} iface;
static int is_xinput(const iface *f)
{
return f->cls == 0xFF && f->sub == 0x5D && f->proto == 0x01;
}
/* HID, but not a boot keyboard (1/1) or mouse (1/2). */
static int is_boot_kbd_mouse(const iface *f)
{
return f->cls == 0x03 && f->sub == 0x01 && (f->proto == 0x01 || f->proto == 0x02);
}
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;
out->mps_in = f->mps_in;
out->ep_out = f->ep_out;
out->mps_out = f->mps_out;
return kind;
}
int usbdesc_choose(const unsigned char *cfg, int len, uint16_t vid, uint16_t pid, usbdesc_pick *out)
{
iface ifs[16];
int n = 0, off = 0, i;
memset(out, 0, sizeof *out);
while (off + 2 <= len) {
const unsigned char *d = cfg + off;
int dl = d[0];
if (dl < 2 || off + dl > len) break;
if (d[1] == DT_INTERFACE && dl >= 9) {
if (n == (int)(sizeof ifs / sizeof ifs[0])) break;
memset(&ifs[n], 0, sizeof ifs[n]);
ifs[n].num = d[2];
ifs[n].cls = d[5];
ifs[n].sub = d[6];
ifs[n].proto = d[7];
n++;
} else if (d[1] == DT_ENDPOINT && dl >= 7 && n > 0 && (d[3] & 3) == 3) {
iface *f = &ifs[n - 1];
uint16_t mps = (uint16_t)(d[4] | d[5] << 8);
if ((d[2] & 0x80) && !f->ep_in) {
f->ep_in = d[2];
f->mps_in = mps;
} else if (!(d[2] & 0x80) && !f->ep_out) {
f->ep_out = d[2];
f->mps_out = mps;
}
}
off += dl;
}
for (i = 0; i < n; i++)
if (is_xinput(&ifs[i]) && ifs[i].ep_in) return pick(ifs, n, &ifs[i], USBPAD_XINPUT, out);
if (vid == 0x057E && pid == 0x2009)
for (i = 0; i < n; i++)
if (ifs[i].cls == 0x03 && ifs[i].ep_in) return pick(ifs, n, &ifs[i], USBPAD_SWITCH, out);
if (vid == VID_8BITDO)
for (i = 0; i < n; i++)
if (is_pad_hid(&ifs[i]) && ifs[i].ep_in) return pick(ifs, n, &ifs[i], USBPAD_8BITDO, out);
return USBPAD_NONE;
}
int usbdesc_string(const unsigned char *d, int len, char *out, size_t size)
{
size_t n = 0;
int i;
if (size == 0) return 0;
out[0] = 0;
if (len < 2 || d[1] != 0x03) return 0;
if (d[0] < len) len = d[0];
for (i = 2; i + 1 < len && n + 1 < size; i += 2) {
unsigned c = d[i] | (unsigned)d[i + 1] << 8;
out[n++] = (char)(c >= 0x20 && c < 0x7F ? c : '?');
}
while (n > 0 && out[n - 1] == ' ') n--;
out[n] = 0;
return n > 0;
}
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;
}
+43
View File
@@ -0,0 +1,43 @@
/* Which interface of a USB device padbridge reads, from its configuration
* descriptor alone (no device needed, so it runs in the tests too). */
#ifndef PADBRIDGE_USBDESC_H
#define PADBRIDGE_USBDESC_H
#include <stddef.h>
#include <stdint.h>
#define VID_8BITDO 0x2DC8
enum {
USBPAD_NONE = 0,
USBPAD_8BITDO, /* 8BitDo DInput: HID report 1 (map8bitdo.c) */
USBPAD_XINPUT, /* Xbox 360 protocol, vendor interface ff/5d/01 (mapxinput.c) */
USBPAD_SWITCH, /* Nintendo Switch Pro Controller, 057e:2009 (mapswitch.c) */
};
typedef struct {
int kind;
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. 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, 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
+394 -44
View File
@@ -1,8 +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>
@@ -15,24 +18,44 @@
#include <string.h>
#include <unistd.h>
#define VID_8BITDO 0x2DC8
#define N_RD 4 /* reads kept pending on the IN endpoint */
#define EP_OUT N_RD /* transfer index of the OUT endpoint, if any */
#define RD_MAX 64
#define T_ALIVE 1000 /* ms between checks that the device is still there */
#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];
uint8_t ep_in;
uint16_t mps;
struct usb_fs_endpoint ep[N_RD];
void *bufp[N_RD][1];
uint32_t lenp[N_RD][1];
unsigned char buf[N_RD][RD_MAX];
unsigned char busy[N_RD];
int kind; /* USBPAD_* (usbdesc.h) */
uint8_t ep_in, ep_out;
uint16_t mps, mps_out;
struct usb_fs_endpoint ep[N_RD + 1];
void *bufp[N_RD + 1][1];
uint32_t lenp[N_RD + 1][1];
unsigned char buf[N_RD + 1][RD_MAX];
unsigned char busy[N_RD + 1];
long t_alive;
int logged_first;
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)
@@ -40,34 +63,102 @@ static int dead_errno(int e)
return e == ENXIO || e == ENODEV || e == EIO || e == EBADF || e == ENOENT;
}
/* The first interrupt IN endpoint of a HID interface. */
static int find_in_endpoint(int fd, uint8_t *ep, uint16_t *mps)
/* The interface to read, from the full configuration descriptor. */
static int choose_interface(int fd, uint16_t vid, uint16_t pid, usbdesc_pick *p)
{
struct usb_gen_descriptor gd;
unsigned char cfg[1024];
int off = 0, len, hid = 0;
int len;
memset(&gd, 0, sizeof gd);
gd.ugd_data = cfg;
gd.ugd_maxlen = sizeof cfg;
gd.ugd_config_index = 0xFF;
if (ioctl(fd, USB_GET_FULL_DESC, &gd) != 0) return 0;
if (ioctl(fd, USB_GET_FULL_DESC, &gd) != 0) return USBPAD_NONE;
len = gd.ugd_actlen < (int)sizeof cfg ? gd.ugd_actlen : (int)sizeof cfg;
while (off + 2 <= len) {
int dl = cfg[off];
const unsigned char *d = cfg + off;
if (dl < 2 || off + dl > len) break;
if (d[1] == 0x04 && dl >= 9) hid = d[5] == 0x03;
else if (d[1] == 0x05 && dl >= 7 && hid && (d[2] & 0x80) && (d[3] & 3) == 3) {
*ep = d[2];
*mps = (uint16_t)(d[4] | d[5] << 8);
return 1;
}
off += dl;
return usbdesc_choose(cfg, len, vid, pid, p);
}
/* Sends one interrupt OUT transfer and waits for it, a short while. */
static int send_out(usbpad *u, const unsigned char *data, int n)
{
struct usb_fs_start st;
struct usb_fs_complete done;
long t0;
if (!u->ep_out || n > u->mps_out || n > RD_MAX) return 0;
memcpy(u->buf[EP_OUT], data, (size_t)n);
u->lenp[EP_OUT][0] = (uint32_t)n;
u->ep[EP_OUT].nFrames = 1;
u->ep[EP_OUT].aFrames = 0;
u->ep[EP_OUT].status = 0;
memset(&st, 0, sizeof st);
st.ep_index = EP_OUT;
if (ioctl(u->fd, USB_FS_START, &st) != 0) return 0;
u->busy[EP_OUT] = 1;
for (t0 = now_ms(); now_ms() - t0 < 100; usleep(1000)) {
memset(&done, 0, sizeof done);
if (ioctl(u->fd, USB_FS_COMPLETE, &done) != 0) continue;
u->busy[done.ep_index] = 0;
if (done.ep_index == EP_OUT) return u->ep[EP_OUT].status == 0;
}
return 0;
}
/* 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)
{
struct usb_ctl_request req;
unsigned char buf[255];
if (index == 0) return 0;
memset(&req, 0, sizeof req);
memset(buf, 0, sizeof buf);
req.ucr_data = buf;
req.ucr_flags = 0x0004; /* USB_SHORT_XFER_OK (usbdi.h, kernel-only header) */
req.ucr_request.bmRequestType = UT_READ_DEVICE;
req.ucr_request.bRequest = UR_GET_DESCRIPTOR;
USETW(req.ucr_request.wValue, 0x0300 | index);
USETW(req.ucr_request.wIndex, 0x0409);
USETW(req.ucr_request.wLength, sizeof buf);
if (ioctl(fd, USB_DO_REQUEST, &req) != 0) return 0;
return usbdesc_string(buf, req.ucr_actlen, out, size);
}
static int start_read(usbpad *u, int i)
{
struct usb_fs_start st;
@@ -83,41 +174,167 @@ 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)
{
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;
if (UGETW(dd.idVendor) != VID_8BITDO) goto fail;
if (!find_in_endpoint(u->fd, &u->ep_in, &u->mps)) goto fail;
u->kind = choose_interface(u->fd, UGETW(dd.idVendor), 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 || 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. A name known for its id wins. */
memset(&di, 0, sizeof di);
if (ioctl(u->fd, USB_GET_DEVICEINFO, &di) == 0 && di.udi_product[0])
snprintf(u->name, sizeof u->name, "%.60s", di.udi_product);
else
snprintf(u->name, sizeof u->name, "8BitDo %04x", UGETW(dd.idProduct));
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
snprintf(u->name, sizeof u->name, "%s", UGETW(dd.idVendor) == VID_8BITDO ? "8BitDo" : "USB controller");
}
for (i = 0; i < N_RD; i++) {
for (i = 0; i <= N_RD; i++) {
u->bufp[i][0] = u->buf[i];
u->lenp[i][0] = u->mps;
u->lenp[i][0] = i == EP_OUT ? u->mps_out : u->mps;
u->ep[i].ppBuffer = u->bufp[i];
u->ep[i].pLength = u->lenp[i];
u->ep[i].nFrames = 1;
u->ep[i].flags = USB_FS_FLAG_SINGLE_SHORT_OK;
u->ep[i].flags = i == EP_OUT ? 0 : USB_FS_FLAG_SINGLE_SHORT_OK;
}
memset(&init, 0, sizeof init);
init.pEndpoints = u->ep;
init.ep_index_max = N_RD;
init.ep_index_max = N_RD + 1;
if (ioctl(u->fd, USB_FS_INIT, &init) != 0) {
log_line("usb: %s: init errno %d", u->path, errno);
goto fail;
@@ -135,9 +352,39 @@ static usbpad *try_node(int bus, int addr)
return NULL;
}
}
if (u->ep_out) {
struct usb_fs_open op;
memset(&op, 0, sizeof op);
op.ep_index = EP_OUT;
op.ep_no = u->ep_out;
op.max_bufsize = u->mps_out;
op.max_frames = 1;
if (ioctl(u->fd, USB_FS_OPEN, &op) != 0) {
log_line("usb: %s: open endpoint %#x errno %d (no output)", u->path, u->ep_out, errno);
u->ep_out = 0;
}
}
u->t_alive = now_ms();
log_line("usb: %s %04x:%04x \"%s\", reading endpoint %#x (%u bytes)", u->path, UGETW(dd.idVendor),
UGETW(dd.idProduct), u->name, u->ep_in, u->mps);
log_line("usb: %s %04x:%04x \"%s\", %s interface %u, reading endpoint %#x (%u bytes)", u->path,
UGETW(dd.idVendor), UGETW(dd.idProduct), u->name, (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:
@@ -148,23 +395,121 @@ 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, &not_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;
}
const char *usbpad_name(const usbpad *u)
{
return u && u->name[0] ? u->name : "USB pad";
return u && u->name[0] ? u->name : "USB controller";
}
const char *usbpad_mode(const usbpad *u)
{
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)
@@ -188,11 +533,14 @@ int usbpad_poll(usbpad *u, pad_state *st)
break;
}
i = done.ep_index;
if (i >= N_RD) continue;
if (i > N_RD) continue;
u->busy[i] = 0;
if (i == EP_OUT) continue;
if (u->ep[i].status) continue; /* cancelled or stalled: read again */
if (!u->ep[i].aFrames) continue;
if (usbpad_parse_8bitdo(u->buf[i], (int)u->lenp[i][0], st)) {
if (u->kind == USBPAD_XINPUT ? usbpad_parse_xinput(u->buf[i], (int)u->lenp[i][0], st)
: 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");
@@ -200,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) {
@@ -218,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);
+25 -9
View File
@@ -2,20 +2,26 @@
* console's own HID driver (which holds the interface but does nothing with
* a device it does not know).
*
* Today: the 8BitDo 2.4G dongle (2dc8:301c). With no controller it shows up
* as "8BitDo IDLE" and sends nothing useful; when the controller connects it
* enumerates again as "8BitDo Ultimate 2C Wireless Controller" and sends
* report 1 (see usbpad.c). Every re-enumeration ends the open device, so the
* caller closes it and looks again.
* Today: the 8BitDo Ultimate 2C 2.4G dongle (2dc8:301c). With no controller it
* shows up as "8BitDo IDLE" and sends nothing useful; when the controller
* connects it enumerates again, as "8BitDo Ultimate 2C Wireless Controller"
* sending HID report 1 (DInput, map8bitdo.c), or, with the controller in
* XInput mode, as 2dc8:310a with an Xbox 360 interface (mapxinput.c). Which
* interface is read is chosen in usbdesc.c. Every re-enumeration ends the
* open device, so the caller closes it and looks again.
*/
#ifndef PAD2C_USBPAD_H
#define PAD2C_USBPAD_H
#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
@@ -23,9 +29,19 @@ usbpad *usbpad_open(void);
int usbpad_poll(usbpad *u, pad_state *st);
void usbpad_close(usbpad *u);
const char *usbpad_name(const 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" */
/* The 8BitDo report 1 to a DualSense state. Returns 0 if it is not one. */
int usbpad_parse_8bitdo(const unsigned char *r, int n, pad_state *st);
/* An XInput (Xbox 360) input report to a DualSense state. 0 if it is not one. */
int usbpad_parse_xinput(const unsigned char *r, int n, pad_state *st);
#endif
+4 -3
View File
@@ -1,6 +1,7 @@
#ifndef PAD2C_VERSION_H
#define PAD2C_VERSION_H
#ifndef PADBRIDGE_VERSION_H
#define PADBRIDGE_VERSION_H
#define PAD2C_VERSION "0.2.1"
#define PADBRIDGE_VERSION "1.3.1"
#define PADBRIDGE_AUTHOR "Sub" /* every notification starts "padbridge by Sub" */
#endif
+243
View File
@@ -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;
}
+39
View File
@@ -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
+73
View File
@@ -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')"
"+' &middot; '+esc(p.mode)+' &middot; slot '+(p.slot+1)+(p.user?' &middot; '+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
+58
View File
@@ -0,0 +1,58 @@
/* View + Menu as the touchpad, for pads without one (XInput). Runs on the PC. */
#include "combo.h"
#include <stdio.h>
static int fails;
#define CHECK(cond, ...) do { if (!(cond)) { printf("FAIL " __VA_ARGS__); printf("\n"); fails++; } } while (0)
#define V PAD_CREATE
#define M PAD_OPTIONS
#define T PAD_TOUCHPAD
int main(void)
{
combo c;
uint32_t o;
long t;
/* both at once: the touchpad, neither of them */
combo_reset(&c);
CHECK(combo_filter(&c, V | M | PAD_CROSS, 0) == (T | PAD_CROSS), "both at once");
CHECK(combo_filter(&c, V | M, 30) == T, "both held");
CHECK(combo_filter(&c, V, 40) == T, "one let go: still the touchpad until both are");
CHECK(combo_filter(&c, 0, 50) == 0, "both let go");
CHECK(combo_filter(&c, V, 200) == 0, "View again: held back");
/* View first, Menu within the window: the touchpad, no View on the way */
combo_reset(&c);
CHECK(combo_filter(&c, V, 0) == 0, "View alone at first: held back");
CHECK(combo_filter(&c, V, 30) == 0, "View alone, 30 ms: held back");
CHECK(combo_filter(&c, V | M, 45) == T, "Menu joins: touchpad");
CHECK(combo_filter(&c, 0, 120) == 0, "released");
/* View held alone past the window: View, as usual */
combo_reset(&c);
combo_filter(&c, V, 0);
CHECK(combo_filter(&c, V, COMBO_WINDOW_MS) == V, "View held past the window");
CHECK(combo_filter(&c, V, 500) == V, "View still held");
CHECK(combo_filter(&c, V | M, 520) == (V | M), "Menu late: both as they are");
CHECK(combo_filter(&c, 0, 600) == 0, "released");
/* a quick tap of Menu, shorter than the window, is not lost */
combo_reset(&c);
CHECK(combo_filter(&c, M, 1000) == 0, "tap: held back");
o = combo_filter(&c, 0, 1030);
CHECK(o == M, "tap released early: sent now (%#x)", (unsigned)o);
for (t = 1031; t < 1030 + COMBO_TAP_MS; t += 10)
if (combo_filter(&c, 0, t) != M) { CHECK(0, "tap not held for its length at %ld", t); break; }
CHECK(combo_filter(&c, 0, 1030 + COMBO_TAP_MS) == 0, "tap over");
/* other buttons go through untouched */
combo_reset(&c);
CHECK(combo_filter(&c, PAD_CROSS | PAD_L1, 0) == (PAD_CROSS | PAD_L1), "others");
printf("%s (%d failure(s))\n", fails ? "FAILED" : "all passed", fails);
return fails != 0;
}
+52
View File
@@ -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;
}
+3
View File
@@ -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) {
+157
View File
@@ -0,0 +1,157 @@
/* The Switch Pro Controller's report 0x30 and its rumble. Runs on the PC. */
#include "mapswitch.h"
#include "usbdesc.h"
#include <stdio.h>
#include <string.h>
static int fails;
#define CHECK(cond, ...) do { if (!(cond)) { printf("FAIL " __VA_ARGS__); printf("\n"); fails++; } } while (0)
/* A report 0x30 with buttons (right, shared, left bytes) and raw 12-bit sticks. */
static void report(unsigned char r[64], int right, int shared, int left, int lx, int ly, int rx, int ry)
{
memset(r, 0, 64);
r[0] = 0x30;
r[3] = (unsigned char)right;
r[4] = (unsigned char)shared;
r[5] = (unsigned char)left;
r[6] = (unsigned char)(lx & 0xFF);
r[7] = (unsigned char)(((lx >> 8) & 0x0F) | ((ly & 0x0F) << 4));
r[8] = (unsigned char)(ly >> 4);
r[9] = (unsigned char)(rx & 0xFF);
r[10] = (unsigned char)(((rx >> 8) & 0x0F) | ((ry & 0x0F) << 4));
r[11] = (unsigned char)(ry >> 4);
}
static uint32_t buttons(int right, int shared, int left)
{
unsigned char r[64];
switch_sticks cal;
pad_state st;
switch_sticks_reset(&cal);
report(r, right, shared, left, 2048, 2048, 2048, 2048);
if (!switch_parse(r, 64, &cal, &st)) return 0xFFFFFFFFu;
return st.buttons;
}
static void test_descriptor(void)
{
static const unsigned char pro[] = {
9, 0x02, 41, 0, 1, 1, 0, 0xC0, 0xFA,
9, 0x04, 0, 0, 2, 0x03, 0, 0, 0,
9, 0x21, 0x11, 0x01, 0, 1, 0x22, 0xCB, 0,
7, 0x05, 0x81, 0x03, 64, 0, 4,
7, 0x05, 0x02, 0x03, 64, 0, 8,
};
usbdesc_pick p;
CHECK(usbdesc_choose(pro, sizeof pro, 0x057E, 0x2009, &p) == USBPAD_SWITCH, "Pro Controller not chosen");
CHECK(p.ep_in == 0x81 && p.ep_out == 0x02 && p.mps_out == 64, "Pro endpoints %#x %#x", p.ep_in, p.ep_out);
CHECK(usbdesc_choose(pro, sizeof pro, 0x057E, 0x2017, &p) == USBPAD_NONE, "another Nintendo pid taken");
}
static void test_buttons(void)
{
/* by position, as on a DualSense: B bottom cross, A right circle, Y left square, X top triangle */
CHECK(buttons(0x04, 0, 0) == PAD_CROSS, "B");
CHECK(buttons(0x08, 0, 0) == PAD_CIRCLE, "A");
CHECK(buttons(0x01, 0, 0) == PAD_SQUARE, "Y");
CHECK(buttons(0x02, 0, 0) == PAD_TRIANGLE, "X");
CHECK(buttons(0x40, 0, 0) == PAD_R1, "R");
CHECK(buttons(0x80, 0, 0) == (PAD_R2), "ZR");
CHECK(buttons(0, 0, 0x40) == PAD_L1, "L");
CHECK(buttons(0, 0, 0x80) == PAD_L2, "ZL");
CHECK(buttons(0, 0x01, 0) == PAD_CREATE, "Minus");
CHECK(buttons(0, 0x02, 0) == PAD_OPTIONS, "Plus");
CHECK(buttons(0, 0x04, 0) == PAD_R3, "R stick");
CHECK(buttons(0, 0x08, 0) == PAD_L3, "L stick");
CHECK(buttons(0, 0x10, 0) == PAD_PS, "Home");
CHECK(buttons(0, 0x20, 0) == PAD_TOUCHPAD, "Capture");
CHECK(buttons(0, 0, 0x01) == PAD_DOWN, "down");
CHECK(buttons(0, 0, 0x02) == PAD_UP, "up");
CHECK(buttons(0, 0, 0x04) == PAD_RIGHT, "right");
CHECK(buttons(0, 0, 0x08) == PAD_LEFT, "left");
CHECK(buttons(0x30, 0, 0x30) == 0, "SL/SR (Joy-Con rail) ignored");
}
static void test_triggers_and_sticks(void)
{
unsigned char r[64];
switch_sticks cal;
pad_state st;
switch_sticks_reset(&cal);
report(r, 0x80, 0, 0x80, 2048, 2048, 2048, 2048);
switch_parse(r, 64, &cal, &st);
CHECK(st.l2 == 255 && st.r2 == 255, "ZL/ZR analog %u %u", st.l2, st.r2);
/* the first report sets the centre: here off 2048, as a real pad is */
switch_sticks_reset(&cal);
report(r, 0, 0, 0, 1990, 2110, 2010, 2070);
switch_parse(r, 64, &cal, &st);
CHECK(st.lx == 128 && st.ly == 128 && st.rx == 128 && st.ry == 128, "centre %u %u %u %u", st.lx, st.ly, st.rx, st.ry);
/* full left and up (y grows upwards on a Switch pad) */
report(r, 0, 0, 0, 1990 - 1500, 2110 + 1500, 2010 + 1500, 2070 - 1500);
switch_parse(r, 64, &cal, &st);
CHECK(st.lx == 0 && st.ly == 0, "left stick up-left %u %u", st.lx, st.ly);
CHECK(st.rx == 255 && st.ry == 255, "right stick down-right %u %u", st.rx, st.ry);
/* half way stays about half way */
report(r, 0, 0, 0, 1990 + 650, 2110, 2010, 2070);
switch_parse(r, 64, &cal, &st);
CHECK(st.lx > 180 && st.lx < 210, "half right %u", st.lx);
/* a reply to a subcommand (0x21) carries the same controller state */
report(r, 0x04, 0, 0, 1990, 2110, 2010, 2070);
r[0] = 0x21;
CHECK(switch_parse(r, 64, &cal, &st) && st.buttons == PAD_CROSS, "0x21 state");
r[0] = 0x81;
CHECK(!switch_parse(r, 64, &cal, &st), "0x81 taken as state");
CHECK(!switch_parse(r, 8, &cal, &st), "a short report taken");
}
static void test_rumble(void)
{
unsigned char d[8];
static const unsigned char neutral[8] = { 0x00, 0x01, 0x40, 0x40, 0x00, 0x01, 0x40, 0x40 };
switch_rumble_encode(0, 0, d);
CHECK(!memcmp(d, neutral, 8), "no rumble is the neutral value");
switch_rumble_encode(255, 255, d);
CHECK(!memcmp(d, d + 4, 4), "both sides the same");
CHECK(d[0] == 0x74 && (d[1] & 0xFE) == 0xC8, "full high band: %02x %02x", d[0], d[1]);
CHECK((d[2] & 0x7F) == 0x3D && d[3] == 0x72, "full low band: %02x %02x", d[2], d[3]);
/* the small motor alone (Crash's spin: 0/63) must reach the low band too:
* third-party pads (the GameSir G8 through the adapter) hardly feel the
* high band */
switch_rumble_encode(0, 63, d);
CHECK((d[1] & 0xFE) != 0, "small motor: high band amplitude (%02x)", d[1]);
CHECK(d[3] != 0x40 || (d[2] & 0x80), "small motor alone: low band silent (%02x %02x)", d[2], d[3]);
/* weak requests are lifted over the motors' threshold: 63 (25%) ends up
* well past the low band's quarter */
switch_rumble_encode(0, 63, d);
CHECK(((d[2] & 0x80) << 1 | d[3]) >= 0x0060, "a weak request stays below the threshold (%02x %02x)", d[2], d[3]);
CHECK(switch_rumble_boost(0) == 0 && switch_rumble_boost(255) == 255, "boost ends");
CHECK(switch_rumble_boost(1) >= SWITCH_RUMBLE_FLOOR, "boost floor");
switch_rumble_encode(128, 0, d);
CHECK(memcmp(d, neutral, 4) != 0, "the large motor alone rumbles");
CHECK((d[1] & 0xFE) == 0x00, "large motor only: no high band amplitude (%02x)", d[1]);
}
int main(void)
{
test_descriptor();
test_buttons();
test_triggers_and_sticks();
test_rumble();
printf("%s (%d failure(s))\n", fails ? "FAILED" : "all passed", fails);
return fails != 0;
}
+177
View File
@@ -0,0 +1,177 @@
/* Interface choice and the XInput mapping. Runs on the PC.
*
* 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"
#include <stdio.h>
#include <string.h>
static int fails;
#define CHECK(cond, ...) do { if (!(cond)) { printf("FAIL " __VA_ARGS__); printf("\n"); fails++; } } while (0)
/* configuration, interface, HID, endpoint descriptors */
#define CFG(total, n) 9, 0x02, (total) & 0xFF, (total) >> 8, n, 1, 0, 0xA0, 0xFA
#define IFACE(i, n, c, s, p) 9, 0x04, i, 0, n, c, s, p, 0
#define HID 9, 0x21, 0x11, 0x01, 0, 1, 0x22, 0x40, 0
#define EP(a, mps, iv) 7, 0x05, a, 0x03, (mps) & 0xFF, (mps) >> 8, iv
static const unsigned char desc_310a[] = {
CFG(9 + 9 + 7 + 7 + 9 + 9 + 7 + 9 + 9 + 7 + 7, 3),
IFACE(0, 2, 0xFF, 0x5D, 0x01), EP(0x84, 32, 1), EP(0x05, 32, 1),
IFACE(1, 1, 0x03, 0x01, 0x01), HID, EP(0x82, 16, 8),
IFACE(2, 2, 0x03, 0x00, 0x00), HID, EP(0x83, 64, 1), EP(0x06, 64, 1),
};
/* The receiver as 0.2.1 reads it: one HID interface. */
static const unsigned char desc_hid[] = {
CFG(9 + 9 + 9 + 7 + 7, 1),
IFACE(0, 2, 0x03, 0x00, 0x00), HID, EP(0x84, 64, 1), EP(0x03, 64, 1),
};
/* A boot keyboard and nothing else. */
static const unsigned char desc_kbd[] = {
CFG(9 + 9 + 9 + 7, 1),
IFACE(0, 1, 0x03, 0x01, 0x01), HID, EP(0x81, 8, 8),
};
static void test_choose(void)
{
usbdesc_pick p;
CHECK(usbdesc_choose(desc_310a, sizeof desc_310a, VID_8BITDO, 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, 0, &p) == USBPAD_XINPUT, "XInput of another vendor");
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, 0, &p) == USBPAD_NONE, "HID of another vendor 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) */
static void xreport(unsigned char r[20], unsigned b, int lt, int rt, int lx, int ly, int rx, int ry)
{
int i;
for (i = 0; i < 20; i++) r[i] = 0;
r[0] = 0x00;
r[1] = 0x14;
r[2] = (unsigned char)b;
r[3] = (unsigned char)(b >> 8);
r[4] = (unsigned char)lt;
r[5] = (unsigned char)rt;
r[6] = (unsigned char)lx; r[7] = (unsigned char)(lx >> 8);
r[8] = (unsigned char)ly; r[9] = (unsigned char)(ly >> 8);
r[10] = (unsigned char)rx; r[11] = (unsigned char)(rx >> 8);
r[12] = (unsigned char)ry; r[13] = (unsigned char)(ry >> 8);
}
static void xexpect(const char *what, unsigned b, uint32_t want)
{
unsigned char r[20];
pad_state st;
xreport(r, b, 0, 0, 0, 0, 0, 0);
if (!usbpad_parse_xinput(r, 20, &st)) {
printf("FAIL %s: not parsed\n", what);
fails++;
} else if (st.buttons != want) {
printf("FAIL %s: buttons %#x, want %#x\n", what, (unsigned)st.buttons, (unsigned)want);
fails++;
}
}
static void test_xinput(void)
{
unsigned char r[20];
pad_state st;
xexpect("idle", 0, 0);
xexpect("up", 0x0001, PAD_UP);
xexpect("down", 0x0002, PAD_DOWN);
xexpect("left", 0x0004, PAD_LEFT);
xexpect("right", 0x0008, PAD_RIGHT);
xexpect("Start", 0x0010, PAD_OPTIONS);
xexpect("Back", 0x0020, PAD_CREATE);
xexpect("L3", 0x0040, PAD_L3);
xexpect("R3", 0x0080, PAD_R3);
xexpect("LB", 0x0100, PAD_L1);
xexpect("RB", 0x0200, PAD_R1);
xexpect("Guide", 0x0400, PAD_PS);
xexpect("A", 0x1000, PAD_CROSS);
xexpect("B", 0x2000, PAD_CIRCLE);
xexpect("X", 0x4000, PAD_SQUARE);
xexpect("Y", 0x8000, PAD_TRIANGLE);
xreport(r, 0, 0, 0, 0, 0, 0, 0);
usbpad_parse_xinput(r, 20, &st);
CHECK(st.lx == 128 && st.ly == 127 && st.rx == 128 && st.ry == 127, "centre %u %u %u %u", st.lx, st.ly, st.rx, st.ry);
xreport(r, 0, 255, 30, -32768, 32767, 32767, -32768);
usbpad_parse_xinput(r, 20, &st);
CHECK(st.lx == 0 && st.ly == 0, "left stick up-left: %u %u", st.lx, st.ly);
CHECK(st.rx == 255 && st.ry == 255, "right stick down-right: %u %u", st.rx, st.ry);
CHECK(st.l2 == 255 && st.r2 == 30, "triggers %u %u", st.l2, st.r2);
CHECK((st.buttons & (PAD_L2 | PAD_R2)) == (PAD_L2 | PAD_R2), "trigger buttons %#x", (unsigned)st.buttons);
xreport(r, 0, 20, 0, 0, 0, 0, 0);
usbpad_parse_xinput(r, 20, &st);
CHECK(!(st.buttons & PAD_L2), "light LT pressed L2");
r[0] = 0x01; r[1] = 0x03; /* LED status, not input */
CHECK(!usbpad_parse_xinput(r, 3, &st), "LED status taken as input");
xreport(r, 0, 0, 0, 0, 0, 0, 0);
CHECK(!usbpad_parse_xinput(r, 13, &st), "short report taken");
}
static void test_string(void)
{
/* string descriptor: length, type 3, UTF-16LE */
static const unsigned char name[] = { 22, 3, '8', 0, 'B', 0, 'i', 0, 't', 0, 'D', 0, 'o', 0, ' ', 0,
'2', 0, 'C', 0, ' ', 0 };
static const unsigned char odd[] = { 8, 3, 'A', 0, 0xE9, 0, 0x3A, 0x26 };
char s[64];
CHECK(usbdesc_string(name, sizeof name, s, sizeof s) && !strcmp(s, "8BitDo 2C"), "string \"%s\"", s);
CHECK(usbdesc_string(odd, sizeof odd, s, sizeof s) && !strcmp(s, "A??"), "non-ASCII \"%s\"", s);
CHECK(usbdesc_string(name, sizeof name, s, 5) && !strcmp(s, "8Bit"), "cut \"%s\"", s);
CHECK(!usbdesc_string(name, 1, s, sizeof s), "1 byte taken");
CHECK(!usbdesc_string((const unsigned char[]){ 4, 2, 'A', 0 }, 4, s, sizeof s), "not a string taken");
CHECK(!usbdesc_string((const unsigned char[]){ 4, 3, ' ', 0 }, 4, s, sizeof s), "blank taken");
}
int main(void)
{
test_string();
test_choose();
test_names();
test_xinput();
printf("%s (%d failure(s))\n", fails ? "FAILED" : "all passed", fails);
return fails != 0;
}
+37
View File
@@ -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;
}
+42
View File
@@ -0,0 +1,42 @@
#!/bin/sh
# Installs the build tools and the PS5 payload SDK on macOS (Homebrew).
#
# Homebrew ships ld.lld in its own formula, outside the directory
# `llvm-config --bindir` names, where the SDK looks for every tool. So the
# tools are linked into one directory, with an llvm-config that names it,
# and the SDK finds that one through LLVM_CONFIG.
set -e
SDK_VERSION=v0.45
SDK_DIR="$HOME/.local/ps5-payload-sdk"
BIN="$HOME/.local/ps5-llvm/bin"
brew install llvm lld
LLVM_BIN="$(brew --prefix llvm)/bin"
LLD_BIN="$(brew --prefix lld)/bin"
mkdir -p "$BIN"
for t in clang clang++ clang-cpp llvm-ar llvm-nm llvm-objcopy llvm-ranlib llvm-strip; do
ln -sf "$LLVM_BIN/$t" "$BIN/$t"
done
ln -sf "$LLD_BIN/ld.lld" "$BIN/ld.lld"
cat > "$BIN/llvm-config" <<EOF
#!/bin/sh
[ "\$1" = "--bindir" ] && { echo "$BIN"; exit 0; }
exec "$LLVM_BIN/llvm-config" "\$@"
EOF
chmod +x "$BIN/llvm-config"
if [ ! -d "$SDK_DIR" ]; then
mkdir -p "$HOME/.local"
cd /tmp
curl -sSL -o ps5-payload-sdk.zip \
"https://github.com/ps5-payload-dev/sdk/releases/download/$SDK_VERSION/ps5-payload-sdk.zip"
unzip -q -o ps5-payload-sdk.zip -d "$HOME/.local"
rm ps5-payload-sdk.zip
fi
cat > "$HOME/.ps5sdk" <<EOF
export PS5_PAYLOAD_SDK="$SDK_DIR"
export LLVM_CONFIG="$BIN/llvm-config"
EOF
echo "ok: . ~/.ps5sdk && make"