1 Commits
Author SHA1 Message Date
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
24 changed files with 1029 additions and 116 deletions

No files matched your search

+18
View File
@@ -1,5 +1,23 @@
# Changelog
## 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
+7 -3
View File
@@ -13,6 +13,10 @@ test:
./build/test_map
cc -std=c11 -Wall -Wextra -Werror -Isrc -o build/test_usb src/usbdesc.c src/mapxinput.c tests/test_usb.c
./build/test_usb
cc -std=c11 -Wall -Wextra -Werror -Isrc -o build/test_config src/config.c tests/test_config.c
./build/test_config
cc -std=c11 -Wall -Wextra -Werror -D_DEFAULT_SOURCE -Isrc -o build/test_web src/web.c src/log.c src/util.c tests/test_web.c
./build/test_web
ifdef PS5_PAYLOAD_SDK
include $(PS5_PAYLOAD_SDK)/toolchain/prospero.mk
@@ -21,15 +25,15 @@ endif
CFLAGS := -std=gnu11 -Wall -Wextra -O2 -Isrc
LDLIBS := -lScePad -lSceUserService -ldl
SRCS := src/main.c src/usbpad.c src/usbdesc.c src/map8bitdo.c src/mapxinput.c src/ps5_vpad.c src/ps5_power.c src/lock.c src/log.c src/util.c
SRCS := src/main.c src/usbpad.c src/usbdesc.c src/map8bitdo.c src/mapxinput.c src/config.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)
@mkdir -p dist
$(CC) $(CFLAGS) -o $@ $(SRCS) $(LDLIBS)
dist/pad2c-probe.elf: src/probe.c
dist/pad2c-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
+51 -14
View File
@@ -1,11 +1,13 @@
# pad2c
**Use an [8BitDo Ultimate 2C Wireless Controller](https://www.8bitdo.com/ultimate-2c-wireless-controller/) on a
jailbroken PS5, as a second controller next to your DualSense.**
**Use an [8BitDo Ultimate 2C Wireless Controller](https://www.8bitdo.com/ultimate-2c-wireless-controller/), or
almost any Bluetooth controller through the [8BitDo USB Wireless Adapter 2](https://www.8bitdo.com/usb-wireless-adapter-2/),
on a jailbroken PS5, next to your DualSense.**
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.
pad2c is a payload for a jailbroken PS5. It reads the 8BitDo 2.4G dongle or the 8BitDo USB Wireless Adapter 2
plugged into the console's USB port and shows each controller to the system and to games as a **virtual
DualSense**, up to four at once. The console's Bluetooth is not involved, so it never touches your DualSense's
pairing. A settings page, opened from a phone or a computer, shows the controllers live.
## Screenshots
@@ -25,6 +27,8 @@ Switching the 8BitDo on and off, with pad2c running on the console:
| [8BitDo Ultimate 2C **Wireless** Controller](https://www.8bitdo.com/ultimate-2c-wireless-controller/) (the one sold for PC / Android, with a 2.4G USB dongle), through its dongle | ✅ |
| 8BitDo Ultimate 2C **Bluetooth** Controller (the Switch edition, no dongle) | ❌ different product |
| The Ultimate 2C Wireless over Bluetooth, without the dongle | ❌ not yet |
| [8BitDo USB Wireless Adapter 2](https://www.8bitdo.com/usb-wireless-adapter-2/), with any controller it pairs with | ✅ tested with a DualSense, the Ultimate 2C (Switch mode) and a GameSir G8 Plus Galileo (Switch mode, with "Nintendo layout") |
| Wired XInput (Xbox 360) controllers on a USB cable | ✅ tested with a GameSir G8 Plus Galileo |
| Other 8BitDo 2.4G dongles | untested: run `pad2c-probe.elf` and open an issue with its output |
## Why this controller, and why the dongle
@@ -50,7 +54,14 @@ controllers. On a jailbroken PS5, pad2c makes the 8BitDo Ultimate 2C appear as a
**Does it work in games, or only in the menus?** In games too. Give the controller its own console user (the console
asks when you press Home) and it is player 2, like a second DualSense.
**Do I lose my DualSense?** No. Both work at the same time. pad2c does not use Bluetooth at all.
**Do I lose my DualSense?** No. Both work at the same time. pad2c does not use the console's Bluetooth at all.
**Can I use my DualSense, an Xbox or a Switch Pro controller through the adapter?** Yes: the USB Wireless Adapter 2
pairs with them over its own Bluetooth, and pad2c reads the adapter. Pairing a DualSense with the adapter unpairs it
from the console; plug it into the console with its cable and press PS to pair it back.
**A/B and X/Y are swapped on my controller.** Some controllers in Switch mode send the Nintendo layout (A on the
right). Tick **Nintendo layout** for it on the settings page.
**Which firmware?** Tested on 13.60 (PS5 Slim). It needs a console that already runs a payload loader such as elfldr
or Payload Manager; it does not jailbreak anything itself.
@@ -60,8 +71,8 @@ or Payload Manager; it does not jailbreak anything itself.
| | |
|---|---|
| Tested on | PS5 Slim (CFI-2114), firmware **13.60**, elfldr 0.26, kstuff-lite 1.11, Payload Manager 0.5.2 |
| Controller | 8BitDo Ultimate 2C Wireless, 2.4G dongle, DInput (`2dc8:301c`) and XInput (`2dc8:310a`) modes |
| Works | all buttons, sticks, analog triggers, d-pad, Home = PS, console menus and games, sleep and wake, the console's "who is using this controller" screen, remembers its user, "turn off" from the console's menu |
| Controllers | 8BitDo Ultimate 2C Wireless, 2.4G dongle, DInput (`2dc8:301c`) and XInput (`2dc8:310a`) modes; 8BitDo USB Wireless Adapter 2, firmware 1.10, XInput (`2dc8:3106`) and DInput (`2dc8:3105`) modes |
| Works | all buttons, sticks, analog triggers, d-pad, Home = PS, console menus and games, sleep and wake, the console's "who is using this controller" screen, remembers its user, "turn off" from the console's menu, up to four controllers at once |
| Not yet | vibration, light bar, motion sensors, audio |
Other firmwares and console models are untested.
@@ -72,6 +83,17 @@ The controller has two modes on its dongle, both work. It keeps the one last cho
until it switches on.
- **XInput** (seen by a PC as an Xbox controller): the same with **X + Home**. L4/R4 send nothing in this mode.
### The 8BitDo USB Wireless Adapter 2
Plug it in and switch the controller on: nothing to set. The adapter picks its mode from the host it sees; pad2c
answers it as Windows does, so it settles in **XInput** by itself, and pad2c hides the keyboard and mouse the adapter
adds in that mode, so the console lists only the controller.
**DInput** works too and also passes a DualSense's touchpad click: with the controller connected, hold **View
(Create on a DualSense) + Left** for 3 seconds (the adapter's light blinks twice). To let the adapter choose by itself
again, hold its button until its light goes off, then pair the controller again. Its DualShock 4 mode does not work
(it stops sending after a moment); pad2c's answer keeps it from ending there.
Notes:
- The 8BitDo has no touchpad: its extra **L4 / R4** buttons press the touchpad.
@@ -94,10 +116,11 @@ Notes:
## Install
1. Plug the dongle into a USB port of the PS5 and switch the controller on in 2.4G mode.
1. Plug the dongle (or the USB Wireless Adapter 2) into a USB port of the PS5 and switch the controller on.
2. Send `pad2c-<version>.elf` to your payload loader (elfldr listens on port 9021), or add it to Payload Manager
and to its autoload list, so that it starts with the console.
3. A notification says it is running. Press **Home** on the 8BitDo: the console asks who is using the controller.
3. A notification says it is running, with the address of the settings page. Press **Home** on the controller: the
console asks who is using it.
For a second player, pick a **second user** (a game takes one controller per user). pad2c remembers the choice
and gives the controller back to that user next time, as a DualSense does.
@@ -107,13 +130,26 @@ Files, all in `/data/pad2c/`:
|---|---|
| `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) |
| `owner`, `owner.1`... | the user the controller in each slot was last given to (delete to forget) |
| `config` | the settings page's choices (`swap <controller name>` for Nintendo layout) |
| `bind_user` | optional: `main`, `other` or a hexadecimal user id, to give the controller to that user at once |
## Settings page
Open **http://&lt;console IP&gt;:8090** in a browser on the same network (the address is in the notification when
pad2c starts). For each controller: its name, mode, slot and user, the **Nintendo layout** switch (A/B and X/Y
swapped, applied at once and kept), and its buttons, sticks and triggers live, to check a mapping. The page is open
to the local network, without a password.
## 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.
- `src/usbpad.c` opens each pad's `/dev/ugenB.A` node and reads its interrupt IN endpoint, up to four pads, each its
own virtual DualSense. The console's own HID driver holds the interface but ignores the device. New pads are
looked for every 100 ms. The internal Bluetooth chip (`ugen0.2`) is never touched.
- The USB Wireless Adapter 2 shows several identities when plugged in (8BitDo XInput, Xbox 360, DualShock 4) and
keeps the one whose host answers like Windows or a Switch. pad2c sends XInput pads what Windows sends first (the
Microsoft OS string and the XUSB request, then the LED), and detaches the console's driver from a boot keyboard or
mouse beside the pad.
- With no controller the dongle shows up as "8BitDo IDLE". When the controller connects, the dongle enumerates again:
in DInput mode it sends HID report 1, turned into DualSense buttons by `src/map8bitdo.c`; in XInput mode it becomes
`2dc8:310a` with an Xbox 360 interface, read by `src/mapxinput.c`. `src/usbdesc.c` picks the interface from the
@@ -122,6 +158,7 @@ Files, all in `/data/pad2c/`:
- `src/ps5_vpad.c` creates the virtual DualSense (`scePadVirtualDevice*`), finds its device id in the kernel log,
gives it to a user (`sceMbusBindDeviceWithUserId`) and feeds it. It follows the kernel log while running: the
console's choice of user is remembered, and its "turn off" (`sceMbusDisconnectDevice`) removes the virtual pad.
- `src/web.c` serves the settings page without blocking the pads; `src/config.c` keeps its choices.
- `src/probe.c` is a read-only diagnostic payload: it dumps the USB descriptors and the reports of whatever is
plugged in. Useful to add support for another USB controller.
@@ -132,7 +169,7 @@ Needs [ps5-payload-sdk](https://github.com/ps5-payload-dev/sdk) and clang, lld a
```
. ~/.ps5sdk
make test # the button maps and the interface choice, on the PC
make test # button maps, interface choice, settings, status JSON, on the PC
make # dist/pad2c-<version>.elf and dist/pad2c-probe.elf
```
+1
View File
@@ -0,0 +1 @@
090d82eab24cdd1af43b82f1610e88d90ba4d6c1335660c3bb953b4ad6dfc50c pad2c-1.0.0.elf
+1
View File
@@ -0,0 +1 @@
9341757afec92e1adc3bf7e3978626548fe2a8dceb6c54544bb4f85b7f704a4f pad2c-probe.elf
+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 @@
/* pad2c's settings, /data/pad2c/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 PAD2C_CONFIG_H
#define PAD2C_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
+146 -61
View File
@@ -14,13 +14,16 @@
* Stop: create /data/pad2c/stop. It also stops by itself before rest mode.
* Log: /data/pad2c/pad2c.log
*/
#include "host.h"
#include "lock.h"
#include "log.h"
#include "ps5_power.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>
@@ -34,8 +37,9 @@
#define LOG_PATH STATE_DIR "/pad2c.log"
#define LOCK_PATH STATE_DIR "/pad2c.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 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,16 +53,108 @@ 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) */
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];
/* 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->st;
}
}
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) vpad_update(slot, &c->st);
c->last_sent = now;
} else if (c->vpad_made && now - c->last_sent >= T_RESEND) {
vpad_update(slot, &c->st); /* keep the pad fed while nothing changes */
c->last_sent = now;
}
}
int main(void)
{
usbpad *pad = NULL;
pad_state st;
const char *why = "stop requested";
long last_scan = -T_SCAN, last_power = 0, last_check = 0, last_sent = 0;
int vpad_made = 0; /* a virtual pad was asked for and not removed since */
int turned_off = 0; /* the console turned it off: Home brings it back */
int ps_latch = 0; /* PS (Home) held since before: not a new press */
char model[80] = "Controller"; /* "<name> (<mode>)" of the last controller, for the notifications */
long last_scan = -T_SCAN, last_power = 0, last_check = 0;
int i;
mkdir(STATE_DIR, 0777);
log_open(LOG_PATH);
@@ -83,67 +179,53 @@ int main(void)
log_close();
return 1;
}
notify("Version %s running\nSwitch the controller on with its dongle plugged in", PAD2C_VERSION);
pad_state_reset(&st);
config_load(CONFIG_PATH);
{
char ip[32];
if (web_start(WEB_PORT) && web_local_ip(ip, sizeof ip))
notify("Version %s running\nSettings: http://%s:%d", PAD2C_VERSION, ip, WEB_PORT);
else
notify("Version %s running\nSwitch the controller on with its dongle plugged in", PAD2C_VERSION);
}
for (i = 0; i < HOST_MAX_PADS; i++) pad_state_reset(&g_ctl[i].st);
while (!g_stop) {
long now = now_ms();
int r = 0;
if (!pad && now - last_scan >= T_SCAN) {
/* New pads, while a slot is free: each gets the first free one. */
if (now - last_scan >= T_SCAN) {
last_scan = now;
pad = usbpad_open();
}
if (pad) {
r = usbpad_poll(pad, &st);
if (r < 0) {
usbpad_close(pad);
pad = NULL;
if (vpad_made) {
vpad_remove(SLOT);
vpad_made = 0;
log_line("controller left");
notify("%s disconnected", model);
for (i = 0; i < HOST_MAX_PADS; i++) {
if (g_ctl[i].pad || g_ctl[i].vpad_made) continue;
if ((g_ctl[i].pad = usbpad_open())) {
g_ctl[i].turned_off = 0;
g_ctl[i].swap = config_swap(usbpad_name(g_ctl[i].pad));
g_ctl[i].user_at = -10000;
log_line("slot %d: %s%s", i, usbpad_name(g_ctl[i].pad), g_ctl[i].swap ? ", A/B and X/Y swapped" : "");
}
turned_off = 0; /* switched off for real: it starts afresh */
pad_state_reset(&st);
} else if (r > 0) {
int ps_new = (st.buttons & PAD_PS) && !ps_latch;
if (!(st.buttons & PAD_PS)) ps_latch = 0;
if (!vpad_made && turned_off) {
if (ps_new) { /* that press only switches it on: not passed on */
vpad_made = vpad_add(SLOT);
turned_off = 0;
ps_latch = 1;
log_line("PS pressed: virtual pad back on");
}
} else if (!vpad_made) {
vpad_made = vpad_add(SLOT);
snprintf(model, sizeof model, "%s (%s)", usbpad_name(pad), usbpad_mode(pad));
log_line("controller connected: %s, virtual pad %s", model,
vpad_made ? "requested" : "refused");
notify("%s connected", model);
}
if (ps_latch) st.buttons &= ~(uint32_t)PAD_PS;
if (vpad_made) vpad_update(SLOT, &st);
last_sent = now;
} else if (vpad_made && now - last_sent >= T_RESEND) {
vpad_update(SLOT, &st); /* keep the pad fed while nothing changes */
last_sent = now;
break;
}
}
for (i = 0; i < HOST_MAX_PADS; i++)
if (g_ctl[i].pad) ctl_poll(i, now);
vpad_poll(now);
{
web_pad w[HOST_MAX_PADS];
web_fill(w, now);
web_poll(w, HOST_MAX_PADS, set_swap);
}
/* Turned off from the console's menu (or the console dropped it): the
* virtual pad goes away, as a DualSense disconnects. */
if (vpad_made && vpad_off(SLOT)) {
vpad_remove(SLOT);
vpad_made = 0;
turned_off = 1;
ps_latch = (st.buttons & PAD_PS) != 0;
log_line("virtual pad turned off; waiting for PS (Home)");
notify("%s turned off\nPress Home to turn it back on", model);
for (i = 0; i < HOST_MAX_PADS; i++) {
ctl *c = &g_ctl[i];
if (!c->vpad_made || !vpad_off(i)) continue;
vpad_remove(i);
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) {
@@ -161,12 +243,15 @@ 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);
for (i = 0; i < HOST_MAX_PADS; i++) {
if (g_ctl[i].vpad_made) vpad_remove(i);
usbpad_close(g_ctl[i].pad);
}
web_stop();
vpad_shutdown();
vpad_creds_restore();
lock_release();
+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];
+44 -9
View File
@@ -8,7 +8,8 @@
* 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.
* 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>
@@ -32,7 +35,7 @@
#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, "pad2c 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("pad2c 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;
}
+47 -12
View File
@@ -69,6 +69,7 @@ int32_t sceUserServiceInitialize(void *params);
int32_t sceUserServiceGetInitialUser(int32_t *user);
int32_t sceUserServiceGetForegroundUser(int32_t *user);
int32_t sceUserServiceGetLoginUserIdList(int32_t list[4]);
int32_t sceUserServiceGetUserName(int32_t user, char *name, size_t size);
int32_t scePadInit(void);
int32_t scePadSetProcessPrivilege(int32_t privilege);
int32_t scePadGetHandle(int32_t user, int32_t type, int32_t index);
@@ -121,29 +122,43 @@ static int32_t g_user = -1;
* (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
* a DualSense does. -1: none yet. */
#define OWNER_PATH "/data/pad2c/owner"
static int32_t g_owner = -1;
#define OWNER_PATH "/data/pad2c/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);
}
@@ -523,18 +538,38 @@ 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();
}
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;
@@ -624,7 +659,7 @@ static void finish_identification(long now)
{
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,
+6
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);
@@ -44,6 +46,10 @@ void vpad_remove(int slot);
* caller removes it and asks for a new one when the user presses PS (Home). */
int vpad_off(int slot);
/* pad2c: the user the slot's live pad is given to, -1 if none (or not known). */
int32_t vpad_user(int slot);
int vpad_user_name(int32_t user, char *out, size_t size); /* 0 if unknown */
/* pad2c: closes the kernel log, held open while running. */
void vpad_shutdown(void);
+24 -5
View File
@@ -17,13 +17,22 @@ static int is_xinput(const iface *f)
}
/* HID, but not a boot keyboard (1/1) or mouse (1/2). */
static int is_pad_hid(const iface *f)
static int is_boot_kbd_mouse(const iface *f)
{
return f->cls == 0x03 && !(f->sub == 0x01 && (f->proto == 0x01 || f->proto == 0x02));
return f->cls == 0x03 && f->sub == 0x01 && (f->proto == 0x01 || f->proto == 0x02);
}
static int pick(const iface *f, int kind, usbdesc_pick *out)
static int is_pad_hid(const iface *f)
{
return f->cls == 0x03 && !is_boot_kbd_mouse(f);
}
static int pick(const iface *ifs, int n, const iface *f, int kind, usbdesc_pick *out)
{
int i;
for (i = 0; i < n; i++)
if (is_boot_kbd_mouse(&ifs[i]) && ifs[i].num < 32) out->hide |= 1u << ifs[i].num;
out->kind = kind;
out->iface = f->num;
out->ep_in = f->ep_in;
@@ -66,10 +75,10 @@ int usbdesc_choose(const unsigned char *cfg, int len, uint16_t vid, usbdesc_pick
}
for (i = 0; i < n; i++)
if (is_xinput(&ifs[i]) && ifs[i].ep_in) return pick(&ifs[i], USBPAD_XINPUT, out);
if (is_xinput(&ifs[i]) && ifs[i].ep_in) return pick(ifs, n, &ifs[i], USBPAD_XINPUT, out);
if (vid == VID_8BITDO)
for (i = 0; i < n; i++)
if (is_pad_hid(&ifs[i]) && ifs[i].ep_in) return pick(&ifs[i], USBPAD_8BITDO, out);
if (is_pad_hid(&ifs[i]) && ifs[i].ep_in) return pick(ifs, n, &ifs[i], USBPAD_8BITDO, out);
return USBPAD_NONE;
}
@@ -90,3 +99,13 @@ int usbdesc_string(const unsigned char *d, int len, char *out, size_t size)
out[n] = 0;
return n > 0;
}
const char *usbdesc_known_name(uint16_t vid, uint16_t pid, const char *manufacturer)
{
static const char adapter2[] = "8BitDo USB Wireless Adapter 2";
int by_8bitdo = manufacturer && !strcmp(manufacturer, "8BitDo");
if (vid == VID_8BITDO && (pid == 0x3105 || pid == 0x3106 || pid == 0x3107)) return adapter2;
if (vid == 0x045E && pid == 0x028E && by_8bitdo) return adapter2; /* its Xbox 360 identity */
return NULL;
}
+6
View File
@@ -19,6 +19,8 @@ typedef struct {
uint8_t iface;
uint8_t ep_in, ep_out; /* ep_out 0: none */
uint16_t mps_in, mps_out;
uint32_t hide; /* bit n: interface n is a boot keyboard or mouse beside the pad
(the USB Wireless Adapter 2's macros), hidden from the console */
} usbdesc_pick;
/* Picks the interface to read:
@@ -32,4 +34,8 @@ int usbdesc_choose(const unsigned char *cfg, int len, uint16_t vid, usbdesc_pick
* 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
+96 -9
View File
@@ -4,6 +4,7 @@
#include "util.h"
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <dev/usb/usb.h>
@@ -20,9 +21,20 @@
#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];
#define T_NOT_PAD 2000 /* ms a node found not to be a pad is left alone, unless it changes */
struct usbpad {
int fd;
int bus, addr;
char path[24];
char name[64];
int kind; /* USBPAD_* (usbdesc.h) */
@@ -84,6 +96,40 @@ static int send_out(usbpad *u, const unsigned char *data, int n)
return 0;
}
/* A control transfer from the device; the bytes read, -1 if it failed. */
static int ctrl_in(int fd, int type, int req, int value, int index, unsigned char *buf, int len)
{
struct usb_ctl_request r;
memset(&r, 0, sizeof r);
r.ucr_data = buf;
r.ucr_flags = 0x0004; /* USB_SHORT_XFER_OK (usbdi.h, kernel-only header) */
r.ucr_request.bmRequestType = (uint8_t)type;
r.ucr_request.bRequest = (uint8_t)req;
USETW(r.ucr_request.wValue, value);
USETW(r.ucr_request.wIndex, index);
USETW(r.ucr_request.wLength, len);
if (ioctl(fd, USB_DO_REQUEST, &r) != 0) return -1;
return r.ucr_actlen;
}
/* What Windows asks of an XInput device when it arrives: the Microsoft OS
* string descriptor (0xEE) and the XUSB driver's vendor request. The USB
* Wireless Adapter 2 picks its mode from the host it sees ("automatic mode
* switch supports Windows and Switch"): answered like this, it stays in
* XInput on its own. Pads that do not know them refuse, which is harmless. */
static void xinput_windows_hello(usbpad *u)
{
unsigned char buf[64];
int os, xusb;
memset(buf, 0, sizeof buf);
os = ctrl_in(u->fd, UT_READ_DEVICE, UR_GET_DESCRIPTOR, 0x03EE, 0, buf, 18);
memset(buf, 0, sizeof buf);
xusb = ctrl_in(u->fd, 0xC1, 0x01, 0x0100, 0x0000, buf, 20);
log_line("usb: %s: Windows hello: OS string %d, XUSB request %d", u->path, os, xusb);
}
/* String descriptor `index` (US English), as ASCII. A standard GET_DESCRIPTOR. */
static int read_string(int fd, int index, char *out, size_t size)
{
@@ -119,24 +165,33 @@ static int start_read(usbpad *u, int i)
return 1;
}
static usbpad *try_node(int bus, int addr)
/* *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" };
struct usb_device_descriptor dd;
struct usb_device_info di;
struct usb_fs_init init;
usbdesc_pick pick;
char maker[32] = "";
const char *known;
usbpad *u;
int i;
u = calloc(1, sizeof *u);
if (!u) return NULL;
u->bus = bus;
u->addr = addr;
snprintf(u->path, sizeof u->path, "/dev/ugen%d.%d", bus, addr);
u->fd = open(u->path, O_RDWR);
if (u->fd < 0) goto fail;
if (ioctl(u->fd, USB_GET_DEVICE_DESC, &dd) != 0) goto fail;
u->kind = choose_interface(u->fd, UGETW(dd.idVendor), &pick);
if (u->kind == USBPAD_NONE) goto fail;
if (u->kind == USBPAD_NONE) {
*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;
@@ -147,9 +202,12 @@ static usbpad *try_node(int bus, int addr)
}
/* The product name: USB_GET_DEVICEINFO fails on the 8BitDo dongle, the
* string descriptor itself does not. */
* string descriptor itself does not. A name known for its id wins. */
memset(&di, 0, sizeof di);
if (!read_string(u->fd, dd.iProduct, u->name, sizeof u->name)) {
read_string(u->fd, dd.iManufacturer, maker, sizeof maker);
if ((known = usbdesc_known_name(UGETW(dd.idVendor), UGETW(dd.idProduct), maker)))
snprintf(u->name, sizeof u->name, "%s", known);
else if (!read_string(u->fd, dd.iProduct, u->name, sizeof u->name)) {
if (ioctl(u->fd, USB_GET_DEVICEINFO, &di) == 0 && di.udi_product[0])
snprintf(u->name, sizeof u->name, "%.60s", di.udi_product);
else
@@ -199,12 +257,23 @@ static usbpad *try_node(int bus, int addr)
u->t_alive = now_ms();
log_line("usb: %s %04x:%04x \"%s\", %s interface %u, reading endpoint %#x (%u bytes)", u->path,
UGETW(dd.idVendor), UGETW(dd.idProduct), u->name, kinds[u->kind], pick.iface, u->ep_in, u->mps);
/* 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_XINPUT) {
xinput_windows_hello(u);
/* The LED of player 1, as a PC sets it: some XInput pads report only after it. */
static const unsigned char led[3] = { 0x01, 0x03, 0x06 };
log_line("usb: %s: XInput LED %s", u->path,
!u->ep_out ? "not sent (no OUT endpoint)" : send_out(u, led, 3) ? "set" : "not acknowledged");
}
g_held[bus][addr] = 1;
return u;
fail:
@@ -215,16 +284,33 @@ 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;
}
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;
}
@@ -292,6 +378,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);
+3 -1
View File
@@ -17,7 +17,9 @@
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
+1 -1
View File
@@ -1,7 +1,7 @@
#ifndef PAD2C_VERSION_H
#define PAD2C_VERSION_H
#define PAD2C_VERSION "1.0.0"
#define PAD2C_VERSION "1.1.0"
#define PAD2C_AUTHOR "Sub" /* every notification starts "pad2c 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\":[", PAD2C_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 @@
/* pad2c'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 PAD2C_WEB_H
#define PAD2C_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 PAD2C_WEBPAGE_H
#define PAD2C_WEBPAGE_H
static const char WEB_PAGE[] =
"<!doctype html><html lang=en><head><meta charset=utf-8>"
"<meta name=viewport content='width=device-width,initial-scale=1'>"
"<title>pad2c by Sub</title><style>"
":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>pad2c by Sub</h1><small id=st>connecting...</small></header>"
"<main id=m><div class=empty>No controller yet.</div></main>"
"<footer>Settings are kept on the console in /data/pad2c/config.</footer>"
"<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='pad2c 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
+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("# pad2c 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) {
+21
View File
@@ -53,6 +53,26 @@ static void test_choose(void)
CHECK(usbdesc_choose(desc_kbd, sizeof desc_kbd, VID_8BITDO, &p) == USBPAD_NONE, "boot keyboard taken");
CHECK(usbdesc_choose(desc_310a, 20, VID_8BITDO, &p) == USBPAD_NONE, "truncated descriptor taken");
/* the boot keyboard beside the pad is to be hidden from the console */
usbdesc_choose(desc_310a, sizeof desc_310a, VID_8BITDO, &p);
CHECK(p.hide == (1u << 1), "310a: hide %#x, want interface 1", (unsigned)p.hide);
usbdesc_choose(desc_hid, sizeof desc_hid, VID_8BITDO, &p);
CHECK(p.hide == 0, "301c: hide %#x", (unsigned)p.hide);
}
static void test_names(void)
{
const char *n;
n = usbdesc_known_name(0x2DC8, 0x3105, "8BitDo");
CHECK(n && !strcmp(n, "8BitDo USB Wireless Adapter 2"), "3105: %s", n ? n : "(none)");
n = usbdesc_known_name(0x2DC8, 0x3106, "8BitDo");
CHECK(n && !strcmp(n, "8BitDo USB Wireless Adapter 2"), "3106: %s", n ? n : "(none)");
n = usbdesc_known_name(0x045E, 0x028E, "8BitDo");
CHECK(n && !strcmp(n, "8BitDo USB Wireless Adapter 2"), "028e from 8BitDo: %s", n ? n : "(none)");
CHECK(!usbdesc_known_name(0x045E, 0x028E, "Microsoft"), "a real Xbox 360 pad renamed");
CHECK(!usbdesc_known_name(0x2DC8, 0x301C, "8BitDo"), "the 2C dongle renamed");
}
/* Xbox 360 report: 00 14, buttons, LT, RT, LX, LY, RX, RY (int16, y up) */
@@ -150,6 +170,7 @@ int main(void)
{
test_string();
test_choose();
test_names();
test_xinput();
printf("%s (%d failure(s))\n", fails ? "FAILED" : "all passed", fails);
return fails != 0;
+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;
}