mirror of
https://github.com/H4zeyaf/Ghostcontrol-PS5-USB-Controller-Patcher-Cyclone2.git
synced 2026-10-06 08:00:16 +02:00
xbox series S support, official NX controllers + refactoring
added support for official Nintendo Switch pro controllers(not just 8bito ultimate 2) and xbox series S
This commit is contained in:
1 parent
c1cb19042d
commit
62e229775e
15 files changed
+1400
-1092
No files matched your search
Binary file not shown.
Binary file not shown.
@@ -1,9 +1,6 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# Ghost-Control: USB HID controller → virtual DualSense on PS5
|
||||
|
||||
PS5_HOST ?= ps5
|
||||
PS5_HOST ?= 192.168.69.44
|
||||
PORT ?= 9021
|
||||
TARGET := ghost-control-ps5.elf
|
||||
TARGET := ghost-control-xbox-ps5.elf
|
||||
|
||||
ifndef PS5_PAYLOAD_SDK
|
||||
$(error PS5_PAYLOAD_SDK is not set)
|
||||
@@ -14,7 +11,7 @@ include $(PS5_PAYLOAD_SDK)/toolchain/prospero.mk
|
||||
CFLAGS += -D__PROSPERO__ -Wall -Wextra -g -O2 -fPIC -fno-stack-protector
|
||||
LDFLAGS += -lScePad -lSceUserService -lpthread -ldl
|
||||
|
||||
SRC := gc_main.c shellui_pad.c
|
||||
SRC := gc_main.c shellui_pad.c usb_helpers.c controller_nintendo.c controller_xbox.c
|
||||
|
||||
.PHONY: all clean deploy
|
||||
|
||||
@@ -27,4 +24,4 @@ clean:
|
||||
rm -f $(TARGET)
|
||||
|
||||
deploy: $(TARGET)
|
||||
nc -w 5 $(PS5_HOST) $(PORT) < $(TARGET)
|
||||
nc -w 8 $(PS5_HOST) $(PORT) < $(TARGET)
|
||||
@@ -0,0 +1,161 @@
|
||||
#include "controller_nintendo.h"
|
||||
#include "usb_helpers.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef __PROSPERO__
|
||||
#include <ps5/klog.h>
|
||||
#define LOG(...) klog_printf("[GC] " __VA_ARGS__)
|
||||
#else
|
||||
#define LOG(...) fprintf(stderr, __VA_ARGS__)
|
||||
#endif
|
||||
|
||||
static uint8_t ntoh_stick(uint16_t v) {
|
||||
if (v > 4095) v = 4095;
|
||||
return (uint8_t)((v * 255u) / 4095u);
|
||||
}
|
||||
|
||||
static uint32_t hat_to_dpad(uint8_t hat) {
|
||||
switch (hat) {
|
||||
case 0: return SCE_PAD_BUTTON_UP;
|
||||
case 1: return SCE_PAD_BUTTON_UP | SCE_PAD_BUTTON_RIGHT;
|
||||
case 2: return SCE_PAD_BUTTON_RIGHT;
|
||||
case 3: return SCE_PAD_BUTTON_DOWN | SCE_PAD_BUTTON_RIGHT;
|
||||
case 4: return SCE_PAD_BUTTON_DOWN;
|
||||
case 5: return SCE_PAD_BUTTON_DOWN | SCE_PAD_BUTTON_LEFT;
|
||||
case 6: return SCE_PAD_BUTTON_LEFT;
|
||||
case 7: return SCE_PAD_BUTTON_UP | SCE_PAD_BUTTON_LEFT;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Report 0x30: full 60Hz stream after handshake
|
||||
* [3]=right btns [4]=shared [5]=left btns [6-8]=lstick [9-11]=rstick */
|
||||
void nintendo_parse_0x30(const uint8_t *b, ScePadData *o) {
|
||||
uint8_t br=b[3], bs=b[4], bl=b[5];
|
||||
uint16_t lx=(uint16_t)(b[6]|((b[7]&0xF)<<8));
|
||||
uint16_t ly=(uint16_t)((b[7]>>4)|((uint16_t)b[8]<<4));
|
||||
uint16_t rx=(uint16_t)(b[9]|((b[10]&0xF)<<8));
|
||||
uint16_t ry=(uint16_t)((b[10]>>4)|((uint16_t)b[11]<<4));
|
||||
uint32_t btn=0;
|
||||
if(br&0x04) btn|=SCE_PAD_BUTTON_CROSS;
|
||||
if(br&0x08) btn|=SCE_PAD_BUTTON_CIRCLE;
|
||||
if(br&0x01) btn|=SCE_PAD_BUTTON_SQUARE;
|
||||
if(br&0x02) btn|=SCE_PAD_BUTTON_TRIANGLE;
|
||||
if(bl&0x40) btn|=SCE_PAD_BUTTON_L1;
|
||||
if(bl&0x80) btn|=SCE_PAD_BUTTON_L2;
|
||||
if(br&0x40) btn|=SCE_PAD_BUTTON_R1;
|
||||
if(br&0x80) btn|=SCE_PAD_BUTTON_R2;
|
||||
if(bs&0x08) btn|=SCE_PAD_BUTTON_L3;
|
||||
if(bs&0x04) btn|=SCE_PAD_BUTTON_R3;
|
||||
if(bs&0x02) btn|=SCE_PAD_BUTTON_OPTIONS;
|
||||
if(bs&0x01) btn|=SCE_PAD_BUTTON_CREATE;
|
||||
if(bs&0x10) btn|=SCE_PAD_BUTTON_PS;
|
||||
if(bs&0x20) btn|=SCE_PAD_BUTTON_TOUCH_PAD;
|
||||
if(bl&0x02) btn|=SCE_PAD_BUTTON_UP;
|
||||
if(bl&0x01) btn|=SCE_PAD_BUTTON_DOWN;
|
||||
if(bl&0x04) btn|=SCE_PAD_BUTTON_RIGHT;
|
||||
if(bl&0x08) btn|=SCE_PAD_BUTTON_LEFT;
|
||||
o->buttons=btn;
|
||||
o->leftStick.x=ntoh_stick(lx); o->leftStick.y=ntoh_stick(ly);
|
||||
o->rightStick.x=ntoh_stick(rx); o->rightStick.y=ntoh_stick(ry);
|
||||
o->analogButtons.l2=(bl&0x80)?255:0;
|
||||
o->analogButtons.r2=(br&0x80)?255:0;
|
||||
o->connected=1; o->quat.w=1.0f;
|
||||
}
|
||||
|
||||
/* Report 0x3f: simple button-change report
|
||||
* [1]=right btns [2]=shared [3]=HAT [4..7]=sticks [8]=L/ZL */
|
||||
void nintendo_parse_0x3f(const uint8_t *b, ScePadData *o) {
|
||||
uint8_t b1=b[1],b2=b[2],hat=b[3],b8=b[8];
|
||||
uint32_t btn=0;
|
||||
if(b1&0x04) btn|=SCE_PAD_BUTTON_CROSS;
|
||||
if(b1&0x08) btn|=SCE_PAD_BUTTON_CIRCLE;
|
||||
if(b1&0x01) btn|=SCE_PAD_BUTTON_SQUARE;
|
||||
if(b1&0x02) btn|=SCE_PAD_BUTTON_TRIANGLE;
|
||||
if(b8&0x40) btn|=SCE_PAD_BUTTON_L1;
|
||||
if(b8&0x80) btn|=SCE_PAD_BUTTON_L2;
|
||||
if(b1&0x40) btn|=SCE_PAD_BUTTON_R1;
|
||||
if(b1&0x80) btn|=SCE_PAD_BUTTON_R2;
|
||||
if(b2&0x08) btn|=SCE_PAD_BUTTON_L3;
|
||||
if(b2&0x04) btn|=SCE_PAD_BUTTON_R3;
|
||||
if(b2&0x02) btn|=SCE_PAD_BUTTON_OPTIONS;
|
||||
if(b2&0x01) btn|=SCE_PAD_BUTTON_CREATE;
|
||||
if(b2&0x10) btn|=SCE_PAD_BUTTON_PS;
|
||||
if(b2&0x20) btn|=SCE_PAD_BUTTON_TOUCH_PAD;
|
||||
btn|=hat_to_dpad(hat);
|
||||
o->buttons=btn;
|
||||
o->leftStick.x=b[4]; o->leftStick.y=b[5];
|
||||
o->rightStick.x=b[6]; o->rightStick.y=b[7];
|
||||
o->analogButtons.l2=(b8&0x80)?255:0;
|
||||
o->analogButtons.r2=(b1&0x80)?255:0;
|
||||
o->connected=1; o->quat.w=1.0f;
|
||||
}
|
||||
|
||||
int nintendo_send_subcmd(int fd, struct usb_fs_endpoint *eps,
|
||||
uint8_t *seq, uint8_t subcmd,
|
||||
const uint8_t *data, uint32_t data_len) {
|
||||
static const uint8_t rumble[8] = {0x00,0x01,0x40,0x40,0x00,0x01,0x40,0x40};
|
||||
uint8_t buf[64];
|
||||
uint32_t len = 11 + data_len;
|
||||
if (len > sizeof(buf)) return -1;
|
||||
memset(buf, 0, sizeof(buf));
|
||||
buf[0] = 0x01; buf[1] = *seq & 0x0f;
|
||||
memcpy(buf+2, rumble, 8);
|
||||
buf[10] = subcmd;
|
||||
if (data_len) memcpy(buf+11, data, data_len);
|
||||
*seq = (uint8_t)((*seq+1) & 0x0f);
|
||||
char tag[16]; snprintf(tag, sizeof(tag), "sc%02x", subcmd);
|
||||
return usb_send_out(fd, &eps[1], buf, len, tag);
|
||||
}
|
||||
|
||||
int nintendo_handle_packet(int fd, struct usb_fs_endpoint *eps,
|
||||
const uint8_t *buf, uint32_t len,
|
||||
int *hs_state, uint8_t *seq,
|
||||
ScePadData *out_pad) {
|
||||
uint8_t rid = buf[0];
|
||||
|
||||
/* Data packets */
|
||||
if ((rid == 0x00 || rid == 0x30) && len >= 12) {
|
||||
if (rid == 0x00 && buf[1] == 0) return 0; /* all-zero artifact */
|
||||
if (*hs_state != HS_STREAMING) *hs_state = HS_STREAMING;
|
||||
nintendo_parse_0x30(buf, out_pad);
|
||||
return 1;
|
||||
}
|
||||
if (rid == 0x3f && len >= 9) {
|
||||
nintendo_parse_0x3f(buf, out_pad);
|
||||
return 1;
|
||||
}
|
||||
if (rid == 0x21 && len >= 12) {
|
||||
LOG("0x21 ACK subcmd=0x%02x hs=%d\n", (buf[12]&0x7f), *hs_state);
|
||||
if (*hs_state == HS_STREAMING) {
|
||||
nintendo_parse_0x30(buf, out_pad);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (rid == 0x81) {
|
||||
if (*hs_state == HS_STREAMING) {
|
||||
LOG("0x81 sub=0x%02x while streaming — reconnect\n", buf[1]);
|
||||
*hs_state = HS_WAIT_81_01;
|
||||
return 0;
|
||||
}
|
||||
if (buf[1] == 0x01 && *hs_state == HS_WAIT_81_01) {
|
||||
usb_send_cmd(fd, &eps[1], 0x80, 0x02);
|
||||
LOG("0x81 0x01 → [80 02]\n");
|
||||
*hs_state = HS_WAIT_81_02;
|
||||
} else if (buf[1] == 0x02 && *hs_state <= HS_WAIT_81_02) {
|
||||
usb_send_cmd(fd, &eps[1], 0x80, 0x04);
|
||||
LOG("0x81 0x02 → [80 04] + subcmds\n");
|
||||
uint8_t d[]={0x01};
|
||||
nintendo_send_subcmd(fd,eps,seq,0x40,d,1);
|
||||
nintendo_send_subcmd(fd,eps,seq,0x48,d,1);
|
||||
nintendo_send_subcmd(fd,eps,seq,0x30,d,1);
|
||||
uint8_t d2[]={0x30};
|
||||
nintendo_send_subcmd(fd,eps,seq,0x03,d2,1);
|
||||
*hs_state = HS_STREAMING;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <dev/usb/usb.h>
|
||||
#include <dev/usb/usb_ioctl.h>
|
||||
#include "gc_types.h"
|
||||
|
||||
/* Handshake states */
|
||||
#define HS_WAIT_81_01 0
|
||||
#define HS_WAIT_81_02 1
|
||||
#define HS_STREAMING 2
|
||||
|
||||
void nintendo_parse_0x30(const uint8_t *b, ScePadData *o);
|
||||
void nintendo_parse_0x3f(const uint8_t *b, ScePadData *o);
|
||||
|
||||
/* Send Nintendo subcommand on OUT ep (eps[1]).
|
||||
* seq: rolling counter, incremented per call. */
|
||||
int nintendo_send_subcmd(int fd, struct usb_fs_endpoint *eps,
|
||||
uint8_t *seq, uint8_t subcmd,
|
||||
const uint8_t *data, uint32_t data_len);
|
||||
|
||||
/* Handle one IN packet in the Nintendo state machine.
|
||||
* Returns 1 if pad was updated and should be injected, 0 otherwise.
|
||||
* hs_state and seq are in/out: updated by the function. */
|
||||
int nintendo_handle_packet(int fd, struct usb_fs_endpoint *eps,
|
||||
const uint8_t *buf, uint32_t len,
|
||||
int *hs_state, uint8_t *seq,
|
||||
ScePadData *out_pad);
|
||||
@@ -0,0 +1,198 @@
|
||||
/* controller_xbox.c — Xbox One S GIP controller for Ghost-Control
|
||||
* All button bit positions hardware-confirmed on PS5 via live GIP probe.
|
||||
* Reference: xboxSeriesSButtonBits.md
|
||||
*/
|
||||
|
||||
#include "controller_xbox.h"
|
||||
#include "usb_helpers.h"
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/ioctl.h>
|
||||
|
||||
#ifdef __PROSPERO__
|
||||
#include <ps5/klog.h>
|
||||
#define LOG(...) klog_printf("[GC] " __VA_ARGS__)
|
||||
#else
|
||||
#define LOG(...) fprintf(stderr, __VA_ARGS__)
|
||||
#endif
|
||||
|
||||
/* Counts GIP INPUT packets — gates Guide button injection at startup */
|
||||
static uint32_t g_input_count = 0;
|
||||
|
||||
/* ── helpers ──────────────────────────────────────────────────────────── */
|
||||
|
||||
static uint8_t trig_scale(uint16_t v) {
|
||||
if (v > 1023u) v = 1023u;
|
||||
return (uint8_t)((v * 255u) / 1023u);
|
||||
}
|
||||
|
||||
#define DEADZONE 7849
|
||||
|
||||
static uint8_t stick_x(int16_t v) {
|
||||
return (v > DEADZONE || v < -DEADZONE) ? (uint8_t)((v + 32768) >> 8) : 128u;
|
||||
}
|
||||
|
||||
static uint8_t stick_y(int16_t v) {
|
||||
return (v > DEADZONE || v < -DEADZONE) ? (uint8_t)(255 - ((v + 32768) >> 8)) : 128u;
|
||||
}
|
||||
|
||||
/* ── input parsing ────────────────────────────────────────────────────── */
|
||||
|
||||
/* Parse GIP INPUT (cmd=0x20, 18 bytes) into ScePadData.
|
||||
* Hardware-confirmed bit positions — see xboxSeriesSButtonBits.md */
|
||||
void xbox_parse_input(const uint8_t *b, ScePadData *o) {
|
||||
uint8_t b4 = b[4];
|
||||
uint8_t b5 = b[5];
|
||||
uint16_t lt16 = (uint16_t)b[6] | ((uint16_t)b[7] << 8);
|
||||
uint16_t rt16 = (uint16_t)b[8] | ((uint16_t)b[9] << 8);
|
||||
int16_t lx = (int16_t)((uint16_t)b[10] | ((uint16_t)b[11] << 8));
|
||||
int16_t ly = (int16_t)((uint16_t)b[12] | ((uint16_t)b[13] << 8));
|
||||
int16_t rx = (int16_t)((uint16_t)b[14] | ((uint16_t)b[15] << 8));
|
||||
int16_t ry = (int16_t)((uint16_t)b[16] | ((uint16_t)b[17] << 8));
|
||||
|
||||
uint8_t lt = trig_scale(lt16);
|
||||
uint8_t rt = trig_scale(rt16);
|
||||
|
||||
o->leftStick.x = stick_x(lx);
|
||||
o->leftStick.y = stick_y(ly);
|
||||
o->rightStick.x = stick_x(rx);
|
||||
o->rightStick.y = stick_y(ry);
|
||||
o->analogButtons.l2 = lt;
|
||||
o->analogButtons.r2 = rt;
|
||||
|
||||
uint32_t btn = 0;
|
||||
|
||||
/* b[4]: system + face buttons */
|
||||
if (b4 & 0x04u) btn |= SCE_PAD_BUTTON_OPTIONS; /* Menu (≡) → Options */
|
||||
if (b4 & 0x08u) btn |= SCE_PAD_BUTTON_SHARE; /* View (⧉) → Share */
|
||||
if (b4 & 0x10u) btn |= SCE_PAD_BUTTON_CROSS; /* A → Cross */
|
||||
if (b4 & 0x20u) btn |= SCE_PAD_BUTTON_CIRCLE; /* B → Circle */
|
||||
if (b4 & 0x40u) btn |= SCE_PAD_BUTTON_SQUARE; /* X → Square */
|
||||
if (b4 & 0x80u) btn |= SCE_PAD_BUTTON_TRIANGLE; /* Y → Triangle */
|
||||
|
||||
/* b[5]: dpad + bumpers + stick clicks */
|
||||
if (b5 & 0x01u) btn |= SCE_PAD_BUTTON_UP;
|
||||
if (b5 & 0x02u) btn |= SCE_PAD_BUTTON_DOWN;
|
||||
if (b5 & 0x04u) btn |= SCE_PAD_BUTTON_LEFT;
|
||||
if (b5 & 0x08u) btn |= SCE_PAD_BUTTON_RIGHT;
|
||||
if (b5 & 0x10u) btn |= SCE_PAD_BUTTON_L1; /* LB → L1 */
|
||||
if (b5 & 0x20u) btn |= SCE_PAD_BUTTON_R1; /* RB → R1 */
|
||||
if (b5 & 0x40u) btn |= SCE_PAD_BUTTON_L3; /* LS → L3 */
|
||||
if (b5 & 0x80u) btn |= SCE_PAD_BUTTON_R3; /* RS → R3 */
|
||||
|
||||
/* Triggers: analog + digital threshold */
|
||||
if (lt > 16u) btn |= SCE_PAD_BUTTON_L2;
|
||||
if (rt > 16u) btn |= SCE_PAD_BUTTON_R2;
|
||||
|
||||
o->buttons = btn;
|
||||
o->connected = 1;
|
||||
o->quat.w = 1.0f;
|
||||
}
|
||||
|
||||
/* ── GIP protocol ─────────────────────────────────────────────────────── */
|
||||
|
||||
static int read_one(int fd, struct usb_fs_endpoint *eps,
|
||||
uint8_t *buf, uint32_t timeout_ms) {
|
||||
void *b[1] = {buf}; uint32_t l[1] = {64};
|
||||
eps[0].ppBuffer = b; eps[0].pLength = l; eps[0].nFrames = 1;
|
||||
eps[0].timeout = timeout_ms; eps[0].aFrames = 0; eps[0].status = 0;
|
||||
eps[0].flags = USB_FS_FLAG_SINGLE_SHORT_OK | USB_FS_FLAG_MULTI_SHORT_OK;
|
||||
|
||||
struct usb_fs_start st; memset(&st, 0, sizeof(st)); st.ep_index = 0;
|
||||
if (ioctl(fd, USB_FS_START, &st) != 0) return -errno;
|
||||
|
||||
int polls = (int)((timeout_ms + 300) / 50) + 1;
|
||||
for (int w = 0; w < polls; w++) {
|
||||
struct usb_fs_complete co; memset(&co, 0, sizeof(co)); co.ep_index = 0;
|
||||
if (ioctl(fd, USB_FS_COMPLETE, &co) == 0) {
|
||||
if (eps[0].aFrames == 0 || l[0] == 0) return 0;
|
||||
return (int)l[0];
|
||||
}
|
||||
if (errno != EBUSY) {
|
||||
struct usb_fs_stop sp; memset(&sp, 0, sizeof(sp)); sp.ep_index = 0;
|
||||
ioctl(fd, USB_FS_STOP, &sp);
|
||||
return -errno;
|
||||
}
|
||||
usleep(50000);
|
||||
}
|
||||
struct usb_fs_stop sp; memset(&sp, 0, sizeof(sp)); sp.ep_index = 0;
|
||||
ioctl(fd, USB_FS_STOP, &sp);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void send_ack(int fd, struct usb_fs_endpoint *eps, uint8_t orig_seq) {
|
||||
uint8_t ack[8] = {GIP_CMD_ACK, 0x00, 0x00, 0x04,
|
||||
orig_seq, GIP_CMD_ANNOUNCE, 0x00, 0x00};
|
||||
usb_send_out(fd, &eps[1], ack, 8, "ack");
|
||||
}
|
||||
|
||||
void xbox_gip_handshake(int fd, struct usb_fs_endpoint *eps) {
|
||||
static const uint8_t power[] = {0x05, 0x20, 0x00, 0x01, 0x00};
|
||||
uint8_t buf[64];
|
||||
int announced = 0;
|
||||
|
||||
g_input_count = 0;
|
||||
|
||||
/* Pass 0: wait for ANNOUNCE; pass 1: send hello to re-trigger */
|
||||
for (int pass = 0; pass < 2 && !announced; pass++) {
|
||||
if (pass == 1) {
|
||||
uint8_t hello[4] = {GIP_CMD_ACK, 0x00, 0x00, 0x00};
|
||||
usb_send_out(fd, &eps[1], hello, 4, "hello");
|
||||
}
|
||||
/* Pass 0: 3 reads — if ANNOUNCE was already sent during probe, we miss it fast.
|
||||
* Pass 1: send hello to re-trigger, wait longer. Handles both direct + hub. */
|
||||
int reads = (pass == 0) ? 3 : 60;
|
||||
for (int i = 0; i < reads && !announced; i++) {
|
||||
int n = read_one(fd, eps, buf, 150);
|
||||
if (n > 0 && buf[0] == GIP_CMD_ANNOUNCE) {
|
||||
send_ack(fd, eps, buf[1]);
|
||||
announced = 1;
|
||||
} else if (n > 0 && buf[0] == GIP_CMD_INPUT) {
|
||||
announced = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
usb_send_out(fd, &eps[1], power, 5, "power");
|
||||
LOG("Xbox handshake done (announced=%d)\n", announced);
|
||||
}
|
||||
|
||||
int xbox_handle_packet(int fd, struct usb_fs_endpoint *eps,
|
||||
const uint8_t *buf, uint32_t len,
|
||||
ScePadData *out_pad) {
|
||||
(void)fd; (void)eps;
|
||||
uint8_t cmd = buf[0];
|
||||
|
||||
/* Player input */
|
||||
if (cmd == GIP_CMD_INPUT && len >= 18) {
|
||||
g_input_count++;
|
||||
xbox_parse_input(buf, out_pad);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Guide button (Xbox logo): cmd=0x07, b[4]=0x01 pressed, 0x00 released.
|
||||
* Controller auto-sends this at connect — gate behind 10 INPUT packets
|
||||
* so the startup auto-send does not inject a PS button press. */
|
||||
if (cmd == 0x07) {
|
||||
out_pad->connected = 1;
|
||||
out_pad->quat.w = 1.0f;
|
||||
out_pad->leftStick.x = 128;
|
||||
out_pad->leftStick.y = 128;
|
||||
out_pad->rightStick.x = 128;
|
||||
out_pad->rightStick.y = 128;
|
||||
if (g_input_count > 10 && len >= 5 && (buf[4] & 0x01u))
|
||||
out_pad->buttons = SCE_PAD_BUTTON_PS;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Re-announce during streaming */
|
||||
if (cmd == GIP_CMD_ANNOUNCE && len >= 4) {
|
||||
static const uint8_t power[] = {0x05, 0x20, 0x00, 0x01, 0x00};
|
||||
send_ack(fd, eps, buf[1]);
|
||||
usb_send_out(fd, &eps[1], power, 5, "repower");
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <dev/usb/usb.h>
|
||||
#include <dev/usb/usb_ioctl.h>
|
||||
#include "gc_types.h"
|
||||
|
||||
/*
|
||||
* Xbox One (VID=0x045E PID=0x02EA) — GIP protocol over USB
|
||||
*
|
||||
* Endpoints: IN=0x82 OUT=0x02 (interface 1, FS speed, maxpkt=64)
|
||||
*
|
||||
* GIP input report format (18 bytes total):
|
||||
* [0]=0x20 cmd [1]=seq [2]=opts [3]=0x0E (payload=14)
|
||||
* [4..5] = buttons uint16 LE
|
||||
* [6..7] = LT uint16 (0-1023)
|
||||
* [8..9] = RT uint16 (0-1023)
|
||||
* [10..11]= LX int16 (center=0)
|
||||
* [12..13]= LY int16
|
||||
* [14..15]= RX int16
|
||||
* [16..17]= RY int16
|
||||
*
|
||||
* GIP wire button layout (from Linux xpad.c, xpad_process_packet_xboxone):
|
||||
* b[4] GIP_BTN1: bit2=View(→Create) bit3=Menu(→Options)
|
||||
* bit4=A(→Cross) bit5=B(→Circle) bit6=X(→Square) bit7=Y(→Triangle)
|
||||
* b[5] GIP_BTN2: bit0=DUp bit1=DDn bit2=DLt bit3=DRt
|
||||
* bit4=LB(→L1) bit5=RB(→R1) bit6=LS(→L3) bit7=RS(→R3)
|
||||
* NOTE: GIP wire bits differ from XInput wButtons constants (XInput driver remaps).
|
||||
* Guide arrives as separate GIP cmd=0x07 packet.
|
||||
*/
|
||||
|
||||
#define XBOX_EP_IN 0x82
|
||||
#define XBOX_EP_OUT 0x02
|
||||
|
||||
/* GIP command bytes */
|
||||
#define GIP_CMD_ACK 0x01
|
||||
#define GIP_CMD_ANNOUNCE 0x02
|
||||
#define GIP_CMD_STATUS 0x03
|
||||
#define GIP_CMD_INPUT 0x20
|
||||
|
||||
/* Parse GIP input report into ScePadData */
|
||||
void xbox_parse_input(const uint8_t *buf, ScePadData *o);
|
||||
|
||||
/* GIP handshake: catch ANNOUNCE → ACK → POWER.
|
||||
* Call immediately after opening IN+OUT endpoints.
|
||||
* eps[0]=IN eps[1]=OUT. */
|
||||
void xbox_gip_handshake(int fd, struct usb_fs_endpoint *eps);
|
||||
|
||||
/* Handle one IN packet. Returns 1 if pad updated, 0 to skip, -1 to reinit. */
|
||||
int xbox_handle_packet(int fd, struct usb_fs_endpoint *eps,
|
||||
const uint8_t *buf, uint32_t len,
|
||||
ScePadData *out_pad);
|
||||
@@ -0,0 +1,783 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* Ghost-Control v5: Multi-controller support
|
||||
* USB HID controllers → virtual DualSense devices on PS5
|
||||
*
|
||||
* Supports up to MAX_SLOTS (4) simultaneous controllers:
|
||||
* - 8BitDo / Nintendo Switch Pro (VID=057E PID=2009)
|
||||
* - Xbox One S (VID=045E PID=02EA)
|
||||
*
|
||||
* Each detected controller gets its own VDA device + force_bind
|
||||
* assignment dialog + VDI injection thread.
|
||||
* Hotplug: plug in any time, disconnect any time.
|
||||
*/
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/time.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
|
||||
#include <dev/usb/usb.h>
|
||||
#include <dev/usb/usb_ioctl.h>
|
||||
#include <dev/usb/usb_endian.h>
|
||||
|
||||
#ifdef __PROSPERO__
|
||||
#include <ps5/kernel.h>
|
||||
#include <ps5/klog.h>
|
||||
#include <ps5/mdbg.h>
|
||||
#endif
|
||||
|
||||
#include "shellui_pad.h"
|
||||
#include "gc_types.h"
|
||||
#include "usb_helpers.h"
|
||||
#include "controller_nintendo.h"
|
||||
#include "controller_xbox.h"
|
||||
|
||||
/* ── Logging ──────────────────────────────────────────────────────────── */
|
||||
#define LOG_DIR "/data/ghostpad"
|
||||
#define LOG_PATH "/data/ghostpad/gc_status.log"
|
||||
#define PID_PATH "/data/ghostpad/gc_main.pid"
|
||||
#define LOG_MAX 480
|
||||
|
||||
static pthread_mutex_t g_log_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static int g_log_fd = -1;
|
||||
|
||||
void ghostpad_status_log_reset(void) {
|
||||
pthread_mutex_lock(&g_log_lock);
|
||||
if (g_log_fd >= 0) { close(g_log_fd); g_log_fd = -1; }
|
||||
mkdir(LOG_DIR, 0755);
|
||||
g_log_fd = open(LOG_PATH, O_WRONLY|O_CREAT|O_TRUNC, 0600);
|
||||
pthread_mutex_unlock(&g_log_lock);
|
||||
}
|
||||
|
||||
void ghostpad_status_log(const char *fmt, ...) {
|
||||
char buf[LOG_MAX]; va_list ap;
|
||||
va_start(ap, fmt); vsnprintf(buf, sizeof(buf)-1, fmt, ap); va_end(ap);
|
||||
buf[LOG_MAX-1] = '\0';
|
||||
klog_printf("%s", buf);
|
||||
pthread_mutex_lock(&g_log_lock);
|
||||
if (g_log_fd >= 0) {
|
||||
size_t n = strnlen(buf, sizeof(buf));
|
||||
write(g_log_fd, buf, n);
|
||||
if (n && buf[n-1] != '\n') write(g_log_fd, "\n", 1);
|
||||
}
|
||||
pthread_mutex_unlock(&g_log_lock);
|
||||
}
|
||||
#define gp_log(...) ghostpad_status_log("[GC] " __VA_ARGS__)
|
||||
|
||||
/* ── SCE stubs ────────────────────────────────────────────────────────── */
|
||||
extern int32_t sceUserServiceInitialize(void *params);
|
||||
extern int32_t sceUserServiceGetInitialUser(int32_t *outUserId);
|
||||
extern int32_t sceUserServiceGetForegroundUser(int32_t *outUserId);
|
||||
extern int32_t scePadInit(void);
|
||||
extern int32_t scePadSetProcessPrivilege(int32_t privilege);
|
||||
extern int32_t scePadGetHandle(int32_t userId, int32_t type, int32_t index);
|
||||
extern int32_t scePadVirtualDeviceAddDevice(void *param, int32_t deviceType);
|
||||
extern int32_t scePadVirtualDeviceDeleteDevice(int32_t handle);
|
||||
extern int32_t scePadVirtualDeviceInsertData(int32_t handle, const void *padData);
|
||||
extern int32_t sceKernelSendNotificationRequest(int unk0, void *req, size_t size, int unk1);
|
||||
|
||||
#define VIRTUAL_DEVICE_TYPE_DUALSENSE 3
|
||||
|
||||
/* ── Multi-controller slots ───────────────────────────────────────────── */
|
||||
#define MAX_SLOTS 4
|
||||
|
||||
typedef struct {
|
||||
volatile int32_t handle; /* VDA handle, -1 = slot free */
|
||||
volatile int vdi_ready;
|
||||
volatile int usb_active; /* USB reader thread is running */
|
||||
volatile int confirmed; /* user pressed a button — assignment done */
|
||||
char dev_path[32]; /* ugen path claimed by this slot */
|
||||
uint16_t vid, pid;
|
||||
volatile uint32_t inject_count;
|
||||
} ctrl_slot_t;
|
||||
|
||||
static ctrl_slot_t g_slots[MAX_SLOTS];
|
||||
static pthread_mutex_t g_slot_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static int32_t g_inject_uid = 0x10000000;
|
||||
|
||||
/* Assignment serialization: only ONE controller may show its assignment
|
||||
* dialog at a time. g_assign_slot = slot awaiting user confirmation, or -1.
|
||||
* Without this, multiple dialogs stack and all bind to the same user. */
|
||||
static volatile int g_assign_slot = -1;
|
||||
|
||||
/* ── klog device-ID queue ─────────────────────────────────────────────── */
|
||||
#define KLOG_QSIZE 16
|
||||
static uint64_t g_klog_q[KLOG_QSIZE];
|
||||
static int g_klog_qw = 0, g_klog_qr = 0;
|
||||
static pthread_mutex_t g_klog_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static void klog_enqueue(uint64_t id) {
|
||||
pthread_mutex_lock(&g_klog_lock);
|
||||
int next = (g_klog_qw + 1) % KLOG_QSIZE;
|
||||
if (next != g_klog_qr) { g_klog_q[g_klog_qw] = id; g_klog_qw = next; }
|
||||
pthread_mutex_unlock(&g_klog_lock);
|
||||
}
|
||||
|
||||
static uint64_t klog_dequeue_ms(int ms) {
|
||||
for (int t = 0; t < ms; t += 100) {
|
||||
pthread_mutex_lock(&g_klog_lock);
|
||||
if (g_klog_qw != g_klog_qr) {
|
||||
uint64_t id = g_klog_q[g_klog_qr];
|
||||
g_klog_qr = (g_klog_qr + 1) % KLOG_QSIZE;
|
||||
pthread_mutex_unlock(&g_klog_lock);
|
||||
return id;
|
||||
}
|
||||
pthread_mutex_unlock(&g_klog_lock);
|
||||
usleep(100000);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── Notification ─────────────────────────────────────────────────────── */
|
||||
typedef struct { char _unk[45]; char message[3075]; } NotifyRequest;
|
||||
static void notify(const char *fmt, ...) {
|
||||
NotifyRequest req; va_list ap;
|
||||
memset(&req, 0, sizeof(req));
|
||||
va_start(ap, fmt); vsnprintf(req.message, sizeof(req.message), fmt, ap); va_end(ap);
|
||||
sceKernelSendNotificationRequest(0, &req, sizeof(req), 0);
|
||||
}
|
||||
|
||||
/* ── klog capture thread ──────────────────────────────────────────────── */
|
||||
static uint64_t parse_hex_str(const char *s) {
|
||||
uint64_t v = 0;
|
||||
while (*s) {
|
||||
char c = *s++;
|
||||
if (c >= '0' && c <= '9') v = (v<<4)|(c-'0');
|
||||
else if (c >= 'a' && c <= 'f') v = (v<<4)|(c-'a'+10);
|
||||
else if (c >= 'A' && c <= 'F') v = (v<<4)|(c-'A'+10);
|
||||
else break;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
static void parse_klog_line(const char *line) {
|
||||
if (!strstr(line, "DEVICE_ADDED")) return;
|
||||
if (!strstr(line, "subType:22")) return;
|
||||
if (!strstr(line, "capabilityBattery:0")) return;
|
||||
const char *p = strstr(line, "DeviceId:0x");
|
||||
if (!p) p = strstr(line, "deviceId=0x");
|
||||
if (!p) return;
|
||||
p += 11;
|
||||
uint64_t id = parse_hex_str(p);
|
||||
if (!id) return;
|
||||
gp_log("klog: VDA device 0x%llx\n", (unsigned long long)id);
|
||||
klog_enqueue(id);
|
||||
}
|
||||
|
||||
static void *klog_capture_thread(void *arg) {
|
||||
(void)arg;
|
||||
char buf[512], line[1024]; size_t line_len = 0;
|
||||
int fd = open("/dev/klog", O_RDONLY);
|
||||
if (fd < 0) {
|
||||
gp_log("klog: /dev/klog errno=%d, trying TCP 127.0.0.1:3232\n", errno);
|
||||
struct sockaddr_in sin; int sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sock < 0) return NULL;
|
||||
memset(&sin,0,sizeof(sin)); sin.sin_family=AF_INET;
|
||||
sin.sin_addr.s_addr=inet_addr("127.0.0.1"); sin.sin_port=htons(3232);
|
||||
for (int r=0; r<10; r++) {
|
||||
if (connect(sock,(struct sockaddr*)&sin,sizeof(sin))==0){fd=sock;gp_log("klog: connected\n");break;}
|
||||
usleep(500000);
|
||||
}
|
||||
if (fd<0){close(sock);return NULL;}
|
||||
}
|
||||
while (1) {
|
||||
ssize_t len = read(fd, buf, sizeof(buf));
|
||||
if (len < 0) { if (errno==EINTR) continue; break; }
|
||||
if (len == 0) { usleep(10000); continue; }
|
||||
for (ssize_t i=0; i<len; i++) {
|
||||
char c = buf[i];
|
||||
if (c=='\n'||line_len>=sizeof(line)-1){line[line_len]='\0';parse_klog_line(line);line_len=0;}
|
||||
else if (c!='\r') line[line_len++]=c;
|
||||
}
|
||||
}
|
||||
close(fd); return NULL;
|
||||
}
|
||||
|
||||
/* ── VDI injection ────────────────────────────────────────────────────── */
|
||||
static void inject_pad(int slot, const ScePadData *pad) {
|
||||
int32_t h = g_slots[slot].handle;
|
||||
if (h < 0 || !g_slots[slot].vdi_ready) return;
|
||||
int vr = scePadVirtualDeviceInsertData(h, pad);
|
||||
uint32_t n = ++g_slots[slot].inject_count;
|
||||
if ((n % 600) == 0)
|
||||
gp_log("slot[%d] VDI #%u ret=0x%08x\n", slot, n, (uint32_t)vr);
|
||||
static int vdi_err_logged = 0;
|
||||
if (vr != 0 && !vdi_err_logged) { gp_log("VDI error 0x%08x\n",(uint32_t)vr); vdi_err_logged=1; }
|
||||
}
|
||||
|
||||
/* ── ugen detection ───────────────────────────────────────────────────── */
|
||||
#define VID_NATIVE 0x2dc8u
|
||||
#define PID_NATIVE 0x310bu
|
||||
#define VID_SWITCH 0x057eu
|
||||
#define PID_SWITCH 0x2009u
|
||||
#define VID_XBOX 0x045eu
|
||||
#define PID_XBOX 0x02eau
|
||||
|
||||
static const char *UGEN_PATHS[] = {
|
||||
"/dev/ugen2.2","/dev/ugen2.3","/dev/ugen2.4","/dev/ugen2.5",
|
||||
"/dev/ugen2.6","/dev/ugen2.7","/dev/ugen2.8","/dev/ugen2.9",
|
||||
"/dev/ugen1.2","/dev/ugen0.2","/dev/ugen0.3",
|
||||
};
|
||||
#define N_UGEN_PATHS ((int)(sizeof(UGEN_PATHS)/sizeof(UGEN_PATHS[0])))
|
||||
|
||||
/* Probe one ugen2.x path to identify controller type.
|
||||
* Returns 1 with vid/pid set, 0 if not a known controller.
|
||||
* Skips non-ugen2 paths (not on external USB bus). */
|
||||
static int probe_one_path(const char *path, uint16_t *out_vid, uint16_t *out_pid) {
|
||||
if (strncmp(path, "/dev/ugen2.", 11) != 0) return 0;
|
||||
|
||||
int fd = open(path, O_RDWR|O_NONBLOCK);
|
||||
if (fd < 0) return 0;
|
||||
|
||||
struct usb_fs_endpoint ep;
|
||||
struct usb_fs_init ini;
|
||||
struct usb_fs_uninit u;
|
||||
memset(&ep,0,sizeof(ep)); memset(&ini,0,sizeof(ini));
|
||||
ini.pEndpoints=&ep; ini.ep_index_max=1;
|
||||
if (ioctl(fd,USB_FS_INIT,&ini)!=0) { close(fd); return 0; }
|
||||
|
||||
int ii=0; ioctl(fd,USB_IFACE_DRIVER_DETACH,&ii);
|
||||
ii=1; ioctl(fd,USB_IFACE_DRIVER_DETACH,&ii);
|
||||
ii=2; ioctl(fd,USB_IFACE_DRIVER_DETACH,&ii);
|
||||
|
||||
struct usb_fs_open po;
|
||||
memset(&po,0,sizeof(po)); po.ep_index=0; po.max_bufsize=64; po.max_frames=1;
|
||||
|
||||
int found = 0;
|
||||
|
||||
/* Nintendo: ep=0x81, maxpkt=64 */
|
||||
po.ep_no=0x81;
|
||||
if (ioctl(fd,USB_FS_OPEN,&po)==0 && po.max_packet_length==64) {
|
||||
gp_log("probe: %s ep=0x81 mpkt=%u → Nintendo\n", path,(unsigned)po.max_packet_length);
|
||||
struct usb_fs_close pc; memset(&pc,0,sizeof(pc)); pc.ep_index=0; ioctl(fd,USB_FS_CLOSE,&pc);
|
||||
*out_vid=VID_SWITCH; *out_pid=PID_SWITCH;
|
||||
found = 1;
|
||||
goto done;
|
||||
}
|
||||
|
||||
/* Xbox One: ep=0x82, maxpkt in (0,64] */
|
||||
memset(&po,0,sizeof(po)); po.ep_index=0; po.max_bufsize=64; po.max_frames=1;
|
||||
po.ep_no=0x82;
|
||||
if (ioctl(fd,USB_FS_OPEN,&po)==0 && po.max_packet_length>0 && po.max_packet_length<=64) {
|
||||
gp_log("probe: %s ep=0x82 mpkt=%u → Xbox One\n", path,(unsigned)po.max_packet_length);
|
||||
struct usb_fs_close pc; memset(&pc,0,sizeof(pc)); pc.ep_index=0; ioctl(fd,USB_FS_CLOSE,&pc);
|
||||
*out_vid=VID_XBOX; *out_pid=PID_XBOX;
|
||||
found = 1;
|
||||
goto done;
|
||||
}
|
||||
|
||||
done:
|
||||
memset(&u,0,sizeof(u)); ioctl(fd,USB_FS_UNINIT,&u);
|
||||
close(fd);
|
||||
return found;
|
||||
}
|
||||
|
||||
/* ── Create VDA and force_bind for a slot ─────────────────────────────── */
|
||||
static int32_t create_vda_for_slot(int slot) {
|
||||
struct { int32_t size; int32_t userId; int32_t pad[6]; } vdp;
|
||||
const int32_t SEN = (int32_t)0xDEADBEEFu;
|
||||
memset(&vdp,0,sizeof(vdp)); vdp.size=sizeof(vdp); vdp.userId=1;
|
||||
for(int k=0;k<6;k++) vdp.pad[k]=SEN;
|
||||
|
||||
int ret = scePadVirtualDeviceAddDevice(&vdp, VIRTUAL_DEVICE_TYPE_DUALSENSE);
|
||||
gp_log("slot[%d] VDA ret=0x%08x\n", slot, (uint32_t)ret);
|
||||
|
||||
int32_t handle = (ret > 0) ? ret : -1;
|
||||
for(int k=0;k<6;k++){
|
||||
if(vdp.pad[k]!=SEN && vdp.pad[k]>0){if(handle<0)handle=vdp.pad[k];break;}
|
||||
}
|
||||
|
||||
uint64_t dev_id = klog_dequeue_ms(10000);
|
||||
if (dev_id) {
|
||||
handle = (int32_t)(dev_id & 0xffffffffu);
|
||||
int br = shellui_pad_force_bind(dev_id, g_inject_uid);
|
||||
gp_log("slot[%d] force_bind(0x%llx, 0x%08x) ret=%d\n",
|
||||
slot, (unsigned long long)dev_id, (uint32_t)g_inject_uid, br);
|
||||
} else if (handle >= 0) {
|
||||
gp_log("slot[%d] klog timeout — using direct handle %d\n", slot, handle);
|
||||
} else {
|
||||
gp_log("slot[%d] GetHandle scan...\n", slot);
|
||||
static const int32_t uids[]={1,0x10000000,(int32_t)0xffffffff};
|
||||
for(int ui=0;ui<3&&handle<0;ui++)
|
||||
for(int idx=0;idx<8&&handle<0;idx++){
|
||||
handle=scePadGetHandle(uids[ui],3,idx);
|
||||
if(handle>=0) gp_log("slot[%d] GetHandle uid=0x%08x idx=%d h=%d\n",
|
||||
slot,(uint32_t)uids[ui],idx,handle);
|
||||
}
|
||||
}
|
||||
if (handle>=0)
|
||||
gp_log("slot[%d] VDI handle=0x%x ready\n", slot, (uint32_t)handle);
|
||||
else
|
||||
gp_log("slot[%d] ERROR: no VDA handle\n", slot);
|
||||
return handle;
|
||||
}
|
||||
|
||||
/* ── USB HID thread ───────────────────────────────────────────────────── */
|
||||
/* Single-session: receives slot+path+vid+pid, runs until disconnect, then exits.
|
||||
* Manager thread handles re-detection after exit. */
|
||||
|
||||
typedef struct {
|
||||
int slot;
|
||||
char dev_path[32];
|
||||
uint16_t vid, pid;
|
||||
} usb_thread_arg_t;
|
||||
|
||||
static void *usb_hid_thread(void *arg) {
|
||||
usb_thread_arg_t *targ = (usb_thread_arg_t *)arg;
|
||||
int slot = targ->slot;
|
||||
char dev_path[32]; memcpy(dev_path, targ->dev_path, sizeof(dev_path));
|
||||
uint16_t vid = targ->vid, pid = targ->pid;
|
||||
free(targ);
|
||||
|
||||
struct usb_fs_endpoint eps[2];
|
||||
struct usb_fs_init init;
|
||||
struct usb_fs_open fs_open;
|
||||
struct usb_fs_start start;
|
||||
struct usb_fs_complete complete;
|
||||
struct usb_fs_stop stop;
|
||||
struct usb_fs_close fs_close;
|
||||
struct usb_fs_uninit uninit;
|
||||
uint8_t buf[64];
|
||||
void *buffers[1]; uint32_t lengths[1];
|
||||
int fd = -1, out_opened = 0;
|
||||
int usb_ready_notified = 0;
|
||||
|
||||
gp_log("slot[%d] USB thread: %s VID=0x%04x PID=0x%04x\n",
|
||||
slot, dev_path, vid, pid);
|
||||
|
||||
/* ── Xbox One: single-pass ─────────────────────────────────────────── */
|
||||
if (pid == PID_XBOX) {
|
||||
fd = open(dev_path, O_RDWR);
|
||||
if (fd < 0) { gp_log("slot[%d] Xbox open fail errno=%d\n", slot, errno); goto exit_slot; }
|
||||
|
||||
{ int ii; for(ii=0;ii<4;ii++){int i2=ii; ioctl(fd,USB_IFACE_DRIVER_DETACH,&i2);} }
|
||||
usleep(120000); /* hub settle: give USB hub time to propagate detach */
|
||||
memset(eps,0,sizeof(eps)); memset(&init,0,sizeof(init));
|
||||
init.pEndpoints=eps; init.ep_index_max=4;
|
||||
if (ioctl(fd,USB_FS_INIT,&init)!=0){
|
||||
gp_log("slot[%d] Xbox FS_INIT fail errno=%d\n",slot,errno);
|
||||
close(fd); goto exit_slot;
|
||||
}
|
||||
|
||||
memset(&fs_open,0,sizeof(fs_open));
|
||||
fs_open.ep_index=0; fs_open.ep_no=XBOX_EP_IN;
|
||||
fs_open.max_bufsize=64; fs_open.max_frames=1;
|
||||
if (ioctl(fd,USB_FS_OPEN,&fs_open)!=0){
|
||||
gp_log("slot[%d] Xbox IN fail errno=%d\n",slot,errno);
|
||||
goto uninit_exit;
|
||||
}
|
||||
gp_log("slot[%d] Xbox IN ep=0x%02x ok maxpkt=%u\n",
|
||||
slot, XBOX_EP_IN, (unsigned)fs_open.max_packet_length);
|
||||
|
||||
buffers[0]=buf; lengths[0]=64;
|
||||
eps[0].ppBuffer=buffers; eps[0].pLength=lengths; eps[0].nFrames=1;
|
||||
eps[0].timeout=50; eps[0].flags=USB_FS_FLAG_SINGLE_SHORT_OK|USB_FS_FLAG_MULTI_SHORT_OK;
|
||||
|
||||
memset(&fs_open,0,sizeof(fs_open));
|
||||
fs_open.ep_index=1; fs_open.ep_no=XBOX_EP_OUT;
|
||||
fs_open.max_bufsize=64; fs_open.max_frames=1;
|
||||
out_opened = (ioctl(fd,USB_FS_OPEN,&fs_open)==0) ? 1 : 0;
|
||||
gp_log("slot[%d] Xbox OUT ep=0x%02x opened=%d\n", slot, XBOX_EP_OUT, out_opened);
|
||||
|
||||
xbox_gip_handshake(fd, eps);
|
||||
goto main_loop;
|
||||
}
|
||||
|
||||
/* ── Nintendo: two-pass ────────────────────────────────────────────── */
|
||||
fd = open(dev_path, O_RDWR);
|
||||
if (fd < 0) { gp_log("slot[%d] open fail errno=%d\n", slot, errno); goto exit_slot; }
|
||||
|
||||
memset(eps,0,sizeof(eps)); memset(&init,0,sizeof(init));
|
||||
init.pEndpoints=eps; init.ep_index_max=1;
|
||||
if (ioctl(fd,USB_FS_INIT,&init)!=0){
|
||||
gp_log("slot[%d] Nintendo FS_INIT p1 fail\n",slot); close(fd); goto exit_slot;
|
||||
}
|
||||
{ int i0=0,i1=1; ioctl(fd,USB_IFACE_DRIVER_DETACH,&i0); ioctl(fd,USB_IFACE_DRIVER_DETACH,&i1); }
|
||||
memset(&fs_open,0,sizeof(fs_open));
|
||||
fs_open.ep_index=0; fs_open.ep_no=0x81; fs_open.max_bufsize=64; fs_open.max_frames=1;
|
||||
if (ioctl(fd,USB_FS_OPEN,&fs_open)!=0){
|
||||
gp_log("slot[%d] Nintendo IN p1 fail errno=%d\n",slot,errno); goto uninit_exit;
|
||||
}
|
||||
gp_log("slot[%d] Nintendo p1 IN ok maxpkt=%u\n",slot,(unsigned)fs_open.max_packet_length);
|
||||
memset(&uninit,0,sizeof(uninit)); ioctl(fd,USB_FS_UNINIT,&uninit);
|
||||
close(fd); fd=-1;
|
||||
|
||||
/* Pass 2: retry DETACH+OPEN up to 5 times to beat usb_hid0 re-attach
|
||||
* (real Switch Pro is claimed by PS5 native HID driver after probe releases it) */
|
||||
{ int detach_try;
|
||||
for (detach_try = 0; detach_try < 5; detach_try++) {
|
||||
fd = open(dev_path, O_RDWR);
|
||||
if (fd < 0) { gp_log("slot[%d] reopen fail attempt %d\n",slot,detach_try); goto exit_slot; }
|
||||
{ int i0=0,i1=1,i2=2;
|
||||
ioctl(fd,USB_IFACE_DRIVER_DETACH,&i0);
|
||||
ioctl(fd,USB_IFACE_DRIVER_DETACH,&i1);
|
||||
ioctl(fd,USB_IFACE_DRIVER_DETACH,&i2); }
|
||||
memset(eps,0,sizeof(eps)); memset(&init,0,sizeof(init));
|
||||
init.pEndpoints=eps; init.ep_index_max=2;
|
||||
if (ioctl(fd,USB_FS_INIT,&init)!=0){
|
||||
close(fd); fd=-1; usleep(50000); continue;
|
||||
}
|
||||
memset(&fs_open,0,sizeof(fs_open));
|
||||
fs_open.ep_index=0; fs_open.ep_no=0x81; fs_open.max_bufsize=64; fs_open.max_frames=1;
|
||||
if (ioctl(fd,USB_FS_OPEN,&fs_open)==0) break; /* claimed it */
|
||||
memset(&uninit,0,sizeof(uninit)); ioctl(fd,USB_FS_UNINIT,&uninit);
|
||||
close(fd); fd=-1;
|
||||
gp_log("slot[%d] Nintendo p2 OPEN retry %d errno=%d\n",slot,detach_try,errno);
|
||||
usleep(50000);
|
||||
}
|
||||
if (fd < 0) { gp_log("slot[%d] Nintendo p2 give up\n",slot); goto exit_slot; }
|
||||
}
|
||||
gp_log("slot[%d] Nintendo p2 IN ok maxpkt=%u\n",slot,(unsigned)fs_open.max_packet_length);
|
||||
if (fs_open.max_packet_length != 64){ gp_log("slot[%d] wrong maxpkt, reinit\n",slot); goto reinit; }
|
||||
|
||||
buffers[0]=buf; lengths[0]=64;
|
||||
eps[0].ppBuffer=buffers; eps[0].pLength=lengths; eps[0].nFrames=1;
|
||||
eps[0].timeout=200; eps[0].flags=USB_FS_FLAG_SINGLE_SHORT_OK|USB_FS_FLAG_MULTI_SHORT_OK;
|
||||
|
||||
/* 8BitDo uses ep=0x02; real Nintendo Switch Pro Controller uses ep=0x01 */
|
||||
memset(&fs_open,0,sizeof(fs_open));
|
||||
fs_open.ep_index=1; fs_open.ep_no=0x02; fs_open.max_bufsize=64; fs_open.max_frames=1;
|
||||
out_opened = (ioctl(fd,USB_FS_OPEN,&fs_open)==0) ? 1 : 0;
|
||||
if (!out_opened) {
|
||||
memset(&fs_open,0,sizeof(fs_open));
|
||||
fs_open.ep_index=1; fs_open.ep_no=0x01; fs_open.max_bufsize=64; fs_open.max_frames=1;
|
||||
out_opened = (ioctl(fd,USB_FS_OPEN,&fs_open)==0) ? 1 : 0;
|
||||
if (out_opened) gp_log("slot[%d] Nintendo OUT ep=0x01 (real Switch Pro)\n", slot);
|
||||
}
|
||||
gp_log("slot[%d] Nintendo OUT opened=%d\n", slot, out_opened);
|
||||
if (out_opened) {
|
||||
usb_send_cmd(fd,&eps[1],0x80,0x02); usleep(30000);
|
||||
usb_send_cmd(fd,&eps[1],0x80,0x04); usleep(50000);
|
||||
gp_log("slot[%d] Nintendo [80 02]+[80 04] sent\n", slot);
|
||||
}
|
||||
|
||||
main_loop: ;
|
||||
int hs_state = (pid==PID_XBOX) ? HS_STREAMING : HS_WAIT_81_01;
|
||||
uint8_t nintendo_seq = 1;
|
||||
|
||||
while (1) {
|
||||
memset(buf,0,64);
|
||||
buffers[0]=buf; lengths[0]=64;
|
||||
eps[0].ppBuffer=buffers; eps[0].pLength=lengths;
|
||||
eps[0].aFrames=0; eps[0].status=0;
|
||||
|
||||
memset(&start,0,sizeof(start)); start.ep_index=0;
|
||||
if (ioctl(fd,USB_FS_START,&start)!=0) {
|
||||
if (errno==EBUSY){
|
||||
memset(&stop,0,sizeof(stop)); stop.ep_index=0; ioctl(fd,USB_FS_STOP,&stop);
|
||||
usleep(5000);
|
||||
} else if (errno==ENXIO||errno==ENOTTY){
|
||||
gp_log("slot[%d] START errno=%d — device gone\n",slot,errno); goto reinit;
|
||||
} else {
|
||||
gp_log("slot[%d] START fatal errno=%d\n",slot,errno); goto reinit;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
int ok=0, cerr=0, cw=0;
|
||||
for(cw=0;cw<60;cw++){
|
||||
memset(&complete,0,sizeof(complete)); complete.ep_index=0;
|
||||
if(ioctl(fd,USB_FS_COMPLETE,&complete)==0){ok=1;break;}
|
||||
cerr=errno;
|
||||
if(cerr==ENXIO||cerr==ENOTTY){gp_log("slot[%d] COMPLETE errno=%d — gone\n",slot,cerr);goto reinit;}
|
||||
if(cerr!=EBUSY) break;
|
||||
usleep(500);
|
||||
}
|
||||
if(!ok){
|
||||
memset(&stop,0,sizeof(stop)); stop.ep_index=0; ioctl(fd,USB_FS_STOP,&stop);
|
||||
continue;
|
||||
}
|
||||
if(lengths[0]<1) continue;
|
||||
|
||||
uint32_t len = lengths[0];
|
||||
|
||||
ScePadData pad; memset(&pad,0,sizeof(pad)); pad.quat.w=1.0f;
|
||||
int injected = 0;
|
||||
|
||||
if (pid == PID_XBOX) {
|
||||
injected = xbox_handle_packet(fd, eps, buf, len, &pad);
|
||||
} else {
|
||||
injected = nintendo_handle_packet(fd, eps, buf, len, &hs_state, &nintendo_seq, &pad);
|
||||
}
|
||||
|
||||
if (injected > 0) {
|
||||
if (!usb_ready_notified) {
|
||||
notify("Ghostcontrol: slot[%d] streaming — controller active", slot);
|
||||
usb_ready_notified = 1;
|
||||
}
|
||||
/* First real button press confirms the assignment — release the gate
|
||||
* so the manager can start the next controller's dialog. */
|
||||
if (!g_slots[slot].confirmed && pad.buttons != 0) {
|
||||
g_slots[slot].confirmed = 1;
|
||||
if (g_assign_slot == slot) g_assign_slot = -1;
|
||||
gp_log("slot[%d] assignment confirmed (button press)\n", slot);
|
||||
}
|
||||
inject_pad(slot, &pad);
|
||||
}
|
||||
}
|
||||
|
||||
reinit:
|
||||
if (usb_ready_notified) { notify("Ghostcontrol: slot[%d] controller disconnected", slot); usb_ready_notified=0; }
|
||||
memset(&stop,0,sizeof(stop)); stop.ep_index=0; ioctl(fd,USB_FS_STOP,&stop);
|
||||
if (out_opened) {
|
||||
memset(&fs_close,0,sizeof(fs_close)); fs_close.ep_index=1; ioctl(fd,USB_FS_CLOSE,&fs_close);
|
||||
out_opened=0;
|
||||
}
|
||||
memset(&fs_close,0,sizeof(fs_close)); fs_close.ep_index=0; ioctl(fd,USB_FS_CLOSE,&fs_close);
|
||||
uninit_exit:
|
||||
memset(&uninit,0,sizeof(uninit)); ioctl(fd,USB_FS_UNINIT,&uninit);
|
||||
close(fd); fd=-1;
|
||||
|
||||
exit_slot:
|
||||
gp_log("slot[%d] USB thread exiting — freeing slot\n", slot);
|
||||
scePadVirtualDeviceDeleteDevice(g_slots[slot].handle);
|
||||
pthread_mutex_lock(&g_slot_lock);
|
||||
g_slots[slot].handle = -1;
|
||||
g_slots[slot].vdi_ready = 0;
|
||||
g_slots[slot].usb_active= 0;
|
||||
g_slots[slot].dev_path[0] = '\0';
|
||||
pthread_mutex_unlock(&g_slot_lock);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ── Controller manager thread ────────────────────────────────────────── */
|
||||
static void *controller_manager_thread(void *arg) {
|
||||
(void)arg;
|
||||
int scan = 0;
|
||||
gp_log("Manager thread started (MAX_SLOTS=%d)\n", MAX_SLOTS);
|
||||
|
||||
while (1) {
|
||||
for (int i = 0; i < N_UGEN_PATHS; i++) {
|
||||
const char *path = UGEN_PATHS[i];
|
||||
|
||||
/* Skip non-external-bus paths */
|
||||
if (strncmp(path, "/dev/ugen2.", 11) != 0) continue;
|
||||
|
||||
/* Serialize assignment: if a controller is still awaiting the user's
|
||||
* button press to confirm its dialog, do not start another one. */
|
||||
if (g_assign_slot >= 0) break;
|
||||
|
||||
/* Skip if already claimed by an active slot */
|
||||
int busy = 0;
|
||||
pthread_mutex_lock(&g_slot_lock);
|
||||
for (int s = 0; s < MAX_SLOTS; s++) {
|
||||
if (g_slots[s].usb_active && strcmp(g_slots[s].dev_path, path) == 0) {
|
||||
busy = 1; break;
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&g_slot_lock);
|
||||
if (busy) continue;
|
||||
|
||||
/* Try to identify controller */
|
||||
uint16_t vid=0, pid=0;
|
||||
if (!probe_one_path(path, &vid, &pid)) continue;
|
||||
|
||||
/* Find free slot */
|
||||
int slot = -1;
|
||||
pthread_mutex_lock(&g_slot_lock);
|
||||
for (int s = 0; s < MAX_SLOTS; s++) {
|
||||
if (g_slots[s].handle < 0 && !g_slots[s].usb_active) { slot=s; break; }
|
||||
}
|
||||
pthread_mutex_unlock(&g_slot_lock);
|
||||
|
||||
if (slot < 0) {
|
||||
if ((scan % 5) == 0) gp_log("manager: all %d slots full\n", MAX_SLOTS);
|
||||
continue;
|
||||
}
|
||||
|
||||
const char *name =
|
||||
(vid==VID_SWITCH && pid==PID_SWITCH) ? "Nintendo Switch Pro / 8BitDo" :
|
||||
(vid==VID_NATIVE && pid==PID_NATIVE) ? "8BitDo Native" :
|
||||
(vid==VID_XBOX && pid==PID_XBOX) ? "Xbox One S" : "Unknown";
|
||||
|
||||
gp_log("manager: %s at %s → slot[%d]\n", name, path, slot);
|
||||
notify("Ghostcontrol: %s detected — assign user on screen", name);
|
||||
|
||||
/* Claim the slot path before VDA so manager skips it if we retry.
|
||||
* Set the assignment gate — released when user confirms (button press). */
|
||||
pthread_mutex_lock(&g_slot_lock);
|
||||
strncpy(g_slots[slot].dev_path, path, sizeof(g_slots[slot].dev_path)-1);
|
||||
g_slots[slot].usb_active = 1; /* tentatively claimed */
|
||||
g_slots[slot].confirmed = 0;
|
||||
g_slots[slot].vid = vid;
|
||||
g_slots[slot].pid = pid;
|
||||
pthread_mutex_unlock(&g_slot_lock);
|
||||
g_assign_slot = slot;
|
||||
|
||||
/* Create VDA and force_bind (shows PS5 assignment dialog) */
|
||||
int32_t handle = create_vda_for_slot(slot);
|
||||
if (handle < 0) {
|
||||
gp_log("manager: slot[%d] VDA failed — releasing\n", slot);
|
||||
pthread_mutex_lock(&g_slot_lock);
|
||||
g_slots[slot].usb_active = 0;
|
||||
g_slots[slot].dev_path[0] = '\0';
|
||||
g_slots[slot].handle = -1;
|
||||
pthread_mutex_unlock(&g_slot_lock);
|
||||
g_assign_slot = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_slot_lock);
|
||||
g_slots[slot].handle = handle;
|
||||
g_slots[slot].vdi_ready = 1;
|
||||
g_slots[slot].inject_count = 0;
|
||||
pthread_mutex_unlock(&g_slot_lock);
|
||||
|
||||
/* Launch USB reader thread */
|
||||
usb_thread_arg_t *targ = malloc(sizeof(*targ));
|
||||
if (!targ) {
|
||||
gp_log("manager: malloc fail for slot[%d]\n", slot);
|
||||
scePadVirtualDeviceDeleteDevice(handle);
|
||||
pthread_mutex_lock(&g_slot_lock);
|
||||
g_slots[slot].handle=-1; g_slots[slot].vdi_ready=0;
|
||||
g_slots[slot].usb_active=0; g_slots[slot].dev_path[0]='\0';
|
||||
pthread_mutex_unlock(&g_slot_lock);
|
||||
g_assign_slot = -1;
|
||||
continue;
|
||||
}
|
||||
targ->slot = slot;
|
||||
strncpy(targ->dev_path, path, sizeof(targ->dev_path)-1);
|
||||
targ->vid=vid; targ->pid=pid;
|
||||
|
||||
pthread_t tid;
|
||||
if (pthread_create(&tid, NULL, usb_hid_thread, targ) != 0) {
|
||||
gp_log("manager: pthread_create fail slot[%d]\n", slot);
|
||||
free(targ);
|
||||
scePadVirtualDeviceDeleteDevice(handle);
|
||||
pthread_mutex_lock(&g_slot_lock);
|
||||
g_slots[slot].handle=-1; g_slots[slot].vdi_ready=0;
|
||||
g_slots[slot].usb_active=0; g_slots[slot].dev_path[0]='\0';
|
||||
pthread_mutex_unlock(&g_slot_lock);
|
||||
g_assign_slot = -1;
|
||||
} else {
|
||||
pthread_detach(tid);
|
||||
gp_log("manager: slot[%d] USB thread started handle=0x%x\n",
|
||||
slot, (uint32_t)handle);
|
||||
notify("Ghostcontrol: slot[%d] ready — press a button to assign", slot);
|
||||
}
|
||||
/* One controller per scan pass — assignment gate blocks the rest
|
||||
* until the user confirms this one with a button press. */
|
||||
break;
|
||||
}
|
||||
|
||||
/* Assignment timeout: if the user never presses a button, release the
|
||||
* gate after ~30s so the queue does not stall forever. */
|
||||
static int assign_wait = 0;
|
||||
if (g_assign_slot >= 0) {
|
||||
if (++assign_wait > 15) { /* 15 * 2s = 30s */
|
||||
gp_log("manager: assignment timeout slot[%d] — releasing gate\n", g_assign_slot);
|
||||
g_assign_slot = -1;
|
||||
assign_wait = 0;
|
||||
}
|
||||
} else {
|
||||
assign_wait = 0;
|
||||
}
|
||||
|
||||
scan++;
|
||||
if ((scan % 5) == 0) {
|
||||
/* Log active slots every 10s */
|
||||
int active = 0;
|
||||
for (int s = 0; s < MAX_SLOTS; s++)
|
||||
if (g_slots[s].usb_active) active++;
|
||||
if (active == 0 && (scan % 10) == 0)
|
||||
gp_log("manager: scan #%d — no controllers\n", scan);
|
||||
}
|
||||
usleep(2000000);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ── Credential elevation ─────────────────────────────────────────────── */
|
||||
static void elevate_credentials(void) {
|
||||
pid_t p = getpid();
|
||||
uint8_t caps[16]; memset(caps,0xff,sizeof(caps));
|
||||
kernel_set_ucred_authid(p, 0x3800000000010003l);
|
||||
kernel_set_ucred_caps(p, caps);
|
||||
}
|
||||
|
||||
/* ── main ─────────────────────────────────────────────────────────────── */
|
||||
int main(void) {
|
||||
int32_t userId=-1, fgUser=-1; int ret;
|
||||
|
||||
ghostpad_status_log_reset();
|
||||
gp_log("Ghost-Control v5 starting — %d slots\n", MAX_SLOTS);
|
||||
notify("Ghostcontrol by StonedModder — plug in controllers now");
|
||||
|
||||
/* Kill previous instance */
|
||||
{ int pfd=open(PID_PATH,O_RDONLY);
|
||||
if(pfd>=0){char pb[16]={0};read(pfd,pb,15);close(pfd);
|
||||
pid_t old=(pid_t)atoi(pb);
|
||||
if(old>0&&old!=getpid()){gp_log("Killing prev pid=%d\n",old);kill(old,SIGTERM);usleep(600000);}
|
||||
}
|
||||
int pfd2=open(PID_PATH,O_WRONLY|O_CREAT|O_TRUNC,0600);
|
||||
if(pfd2>=0){char pb[16];snprintf(pb,sizeof(pb),"%d",getpid());write(pfd2,pb,strlen(pb));close(pfd2);}
|
||||
}
|
||||
|
||||
/* Init slots */
|
||||
for (int s = 0; s < MAX_SLOTS; s++) {
|
||||
g_slots[s].handle = -1;
|
||||
g_slots[s].vdi_ready = 0;
|
||||
g_slots[s].usb_active = 0;
|
||||
g_slots[s].confirmed = 0;
|
||||
g_slots[s].dev_path[0]= '\0';
|
||||
}
|
||||
g_assign_slot = -1;
|
||||
|
||||
sceUserServiceInitialize(NULL);
|
||||
sceUserServiceGetInitialUser(&userId);
|
||||
sceUserServiceGetForegroundUser(&fgUser);
|
||||
gp_log("userId=0x%08x fgUser=0x%08x\n", (uint32_t)userId, (uint32_t)fgUser);
|
||||
g_inject_uid = (fgUser > 0) ? fgUser : userId;
|
||||
if ((uint32_t)userId<0x10000000u||(uint32_t)userId>0x1000000Fu) userId=0x10000000;
|
||||
gp_log("inject_uid=0x%08x\n", (uint32_t)g_inject_uid);
|
||||
|
||||
elevate_credentials();
|
||||
|
||||
ret=scePadInit(); gp_log("scePadInit: 0x%08x\n", ret);
|
||||
ret=scePadSetProcessPrivilege(1); gp_log("scePadSetProcessPrivilege: 0x%08x\n", ret);
|
||||
|
||||
/* Clean up any orphaned VDA devices */
|
||||
for (int dh=0; dh<64; dh++) {
|
||||
if (scePadVirtualDeviceDeleteDevice(dh)==0) gp_log("deleteDevice(%d)\n", dh);
|
||||
}
|
||||
|
||||
/* Start klog capture thread first — must be running before any VDA call */
|
||||
pthread_t klog_tid;
|
||||
if (pthread_create(&klog_tid, NULL, klog_capture_thread, NULL)==0) {
|
||||
pthread_detach(klog_tid);
|
||||
gp_log("klog thread started\n");
|
||||
}
|
||||
usleep(300000); /* let klog thread connect before first VDA */
|
||||
|
||||
/* Start controller manager — handles all detection, VDA creation, USB threads */
|
||||
pthread_t mgr_tid;
|
||||
if (pthread_create(&mgr_tid, NULL, controller_manager_thread, NULL)==0) {
|
||||
pthread_detach(mgr_tid);
|
||||
gp_log("Manager thread started\n");
|
||||
}
|
||||
|
||||
/* Keep-alive */
|
||||
uint32_t tick = 0;
|
||||
while (1) {
|
||||
usleep(1000000);
|
||||
tick++;
|
||||
if (tick % 10 == 0) {
|
||||
int active = 0;
|
||||
for (int s = 0; s < MAX_SLOTS; s++) if (g_slots[s].usb_active) active++;
|
||||
gp_log("alive tick=%u active_slots=%d\n", tick, active);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
|
||||
/* SCE pad button constants */
|
||||
#define SCE_PAD_BUTTON_L3 0x00000002u
|
||||
#define SCE_PAD_BUTTON_R3 0x00000004u
|
||||
#define SCE_PAD_BUTTON_OPTIONS 0x00000008u
|
||||
#define SCE_PAD_BUTTON_UP 0x00000010u
|
||||
#define SCE_PAD_BUTTON_RIGHT 0x00000020u
|
||||
#define SCE_PAD_BUTTON_DOWN 0x00000040u
|
||||
#define SCE_PAD_BUTTON_LEFT 0x00000080u
|
||||
#define SCE_PAD_BUTTON_L2 0x00000100u
|
||||
#define SCE_PAD_BUTTON_R2 0x00000200u
|
||||
#define SCE_PAD_BUTTON_L1 0x00000400u
|
||||
#define SCE_PAD_BUTTON_R1 0x00000800u
|
||||
#define SCE_PAD_BUTTON_TRIANGLE 0x00001000u
|
||||
#define SCE_PAD_BUTTON_CIRCLE 0x00002000u
|
||||
#define SCE_PAD_BUTTON_CROSS 0x00004000u
|
||||
#define SCE_PAD_BUTTON_SQUARE 0x00008000u
|
||||
#define SCE_PAD_BUTTON_CREATE 0x00010000u /* = PS button — triggers home screen via VDI */
|
||||
#define SCE_PAD_BUTTON_PS 0x00010000u
|
||||
#define SCE_PAD_BUTTON_SHARE 0x00000001u /* DualSense Create/Share (DS4 SELECT/SHARE bit) */
|
||||
#define SCE_PAD_BUTTON_TOUCH_PAD 0x00100000u
|
||||
|
||||
typedef struct { uint16_t x; uint16_t y; uint8_t finger; uint8_t pad[3]; } ScePadTouch;
|
||||
typedef struct {
|
||||
uint8_t fingers; uint8_t pad1[3]; uint32_t pad2; ScePadTouch touch[2];
|
||||
} ScePadTouchData;
|
||||
typedef struct {
|
||||
uint32_t buttons;
|
||||
struct { uint8_t x; uint8_t y; } leftStick;
|
||||
struct { uint8_t x; uint8_t y; } rightStick;
|
||||
struct { uint8_t l2; uint8_t r2; } analogButtons;
|
||||
uint16_t padding;
|
||||
struct { float x, y, z, w; } quat;
|
||||
struct { float x, y, z; } vel;
|
||||
struct { float x, y, z; } accel;
|
||||
ScePadTouchData touchData;
|
||||
uint8_t connected;
|
||||
uint8_t _align[3];
|
||||
uint64_t timestamp;
|
||||
uint8_t ext[16];
|
||||
uint8_t count;
|
||||
uint8_t unknown[15];
|
||||
} ScePadData;
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,56 @@
|
||||
#include "usb_helpers.h"
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef __PROSPERO__
|
||||
#include <ps5/klog.h>
|
||||
#define LOG(...) klog_printf("[GC] " __VA_ARGS__)
|
||||
#else
|
||||
#define LOG(...) fprintf(stderr, __VA_ARGS__)
|
||||
#endif
|
||||
|
||||
int usb_send_out(int fd, struct usb_fs_endpoint *ep,
|
||||
const uint8_t *data, uint32_t len, const char *tag) {
|
||||
void *bufs[1] = { (void *)data };
|
||||
uint32_t lens[1] = { len };
|
||||
struct usb_fs_start start;
|
||||
struct usb_fs_complete complete;
|
||||
|
||||
ep->ppBuffer = bufs;
|
||||
ep->pLength = lens;
|
||||
ep->nFrames = 1;
|
||||
ep->timeout = 150;
|
||||
ep->flags = 0;
|
||||
ep->aFrames = 0;
|
||||
ep->status = 0;
|
||||
|
||||
memset(&start, 0, sizeof(start));
|
||||
start.ep_index = 1;
|
||||
if (ioctl(fd, USB_FS_START, &start) != 0) {
|
||||
LOG("OUT %s START fail errno=%d\n", tag, errno);
|
||||
return -errno;
|
||||
}
|
||||
for (int w = 0; w < 20; w++) {
|
||||
memset(&complete, 0, sizeof(complete));
|
||||
complete.ep_index = 1;
|
||||
if (ioctl(fd, USB_FS_COMPLETE, &complete) == 0)
|
||||
return 0;
|
||||
if (errno != EBUSY) {
|
||||
LOG("OUT %s COMPLETE fail errno=%d\n", tag, errno);
|
||||
return -errno;
|
||||
}
|
||||
usleep(50000);
|
||||
}
|
||||
LOG("OUT %s timeout\n", tag);
|
||||
return -EBUSY;
|
||||
}
|
||||
|
||||
int usb_send_cmd(int fd, struct usb_fs_endpoint *ep, uint8_t a, uint8_t b) {
|
||||
uint8_t buf[2] = { a, b };
|
||||
char tag[8];
|
||||
snprintf(tag, sizeof(tag), "%02x%02x", a, b);
|
||||
return usb_send_out(fd, ep, buf, 2, tag);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <dev/usb/usb.h>
|
||||
#include <dev/usb/usb_ioctl.h>
|
||||
|
||||
int usb_send_out(int fd, struct usb_fs_endpoint *ep,
|
||||
const uint8_t *data, uint32_t len, const char *tag);
|
||||
int usb_send_cmd(int fd, struct usb_fs_endpoint *ep, uint8_t a, uint8_t b);
|
||||
@@ -0,0 +1,67 @@
|
||||
# Xbox One S / Series S GIP Wire Button Bits
|
||||
**VID=0x045E PID=0x02EA — hardware-confirmed on PS5 via live GIP probe**
|
||||
|
||||
## GIP INPUT packet (cmd=0x20) layout
|
||||
```
|
||||
b[0]=0x20 b[1]=seq b[2]=opts b[3]=0x0E (payload len=14)
|
||||
b[4] = button byte 1 (digital buttons)
|
||||
b[5] = button byte 2 (dpad + bumpers + sticks)
|
||||
b[6..7] = LT uint16 LE 0-1023 → scale to 0-255
|
||||
b[8..9] = RT uint16 LE 0-1023 → scale to 0-255
|
||||
b[10..11]= LX int16 LE center≈0
|
||||
b[12..13]= LY int16 LE center≈0
|
||||
b[14..15]= RX int16 LE center≈0
|
||||
b[16..17]= RY int16 LE center≈0
|
||||
```
|
||||
|
||||
## b[4] — face buttons + system buttons
|
||||
| Bit | Mask | Physical button | PS5 target |
|
||||
|------|------|-----------------|-------------------|
|
||||
| bit2 | 0x04 | Menu (≡) | OPTIONS (0x0008) |
|
||||
| bit3 | 0x08 | View (⧉) | SHARE (0x20000)|
|
||||
| bit4 | 0x10 | A | CROSS (0x4000) |
|
||||
| bit5 | 0x20 | B | CIRCLE (0x2000) |
|
||||
| bit6 | 0x40 | X | SQUARE (0x8000) |
|
||||
| bit7 | 0x80 | Y | TRIANGLE (0x1000) |
|
||||
|
||||
## b[5] — dpad + bumpers + stick clicks
|
||||
| Bit | Mask | Physical button | PS5 target |
|
||||
|------|------|-----------------|----------------|
|
||||
| bit0 | 0x01 | DPad Up | UP (0x0010) |
|
||||
| bit1 | 0x02 | DPad Down | DOWN (0x0040) |
|
||||
| bit2 | 0x04 | DPad Left | LEFT (0x0080) |
|
||||
| bit3 | 0x08 | DPad Right | RIGHT (0x0020) |
|
||||
| bit4 | 0x10 | LB | L1 (0x0400) |
|
||||
| bit5 | 0x20 | RB | R1 (0x0800) |
|
||||
| bit6 | 0x40 | L3 (left click) | L3 (0x0002) |
|
||||
| bit7 | 0x80 | R3 (right click)| R3 (0x0004) |
|
||||
|
||||
## Analog
|
||||
| Field | Range | PS5 field |
|
||||
|-------------|--------|---------------------|
|
||||
| LT (b[6..7])| 0-1023 | analogButtons.l2 (0-255), digital L2 bit if >16 |
|
||||
| RT (b[8..9])| 0-1023 | analogButtons.r2 (0-255), digital R2 bit if >16 |
|
||||
| LX (b[10..11])| int16 center≈0 | leftStick.x = (lx+32768)>>8, deadzone ±7849 |
|
||||
| LY (b[12..13])| int16 center≈0 | leftStick.y = 255-((ly+32768)>>8), deadzone ±7849 |
|
||||
| RX (b[14..15])| int16 center≈0 | rightStick.x = (rx+32768)>>8, deadzone ±7849 |
|
||||
| RY (b[16..17])| int16 center≈0 | rightStick.y = 255-((ry+32768)>>8), deadzone ±7849 |
|
||||
|
||||
## Guide button (Xbox logo) — HARDWARE CONFIRMED
|
||||
```
|
||||
GIP cmd=0x07 len=6
|
||||
b[4]=0x01 = pressed
|
||||
b[4]=0x00 = released
|
||||
b[5]=0x5b (constant — controller status byte, ignore)
|
||||
```
|
||||
Maps to SCE_PAD_BUTTON_PS (0x10000).
|
||||
Current code: `if (cmd == 0x07 && buf[4] & 0x01u)` — correct.
|
||||
|
||||
## Notes
|
||||
- xpad.c (Linux) labels bit2=View, bit3=Menu — WRONG for this hardware.
|
||||
Confirmed physically: bit2=Menu(≡), bit3=View(⧉).
|
||||
- SCE_PAD_BUTTON_CREATE = SCE_PAD_BUTTON_PS = 0x10000 — injecting this
|
||||
triggers PS home screen. View maps to SHARE (0x20000) instead.
|
||||
- GIP wire bits differ completely from XInput wButtons API constants.
|
||||
XInput is a Windows abstraction layer; these are raw USB GIP bytes.
|
||||
- Idle stick drift observed: lx≈-1863, ly≈-149, rx≈2007, ry≈-771.
|
||||
Deadzone of ±7849 covers this entirely.
|
||||
Reference in new issue
Block a user