Bläddra i källkod

Add MODE_PS3: DualShock 3 personality with gyro and haptics

copilot-swe-agent[bot] 4 månader sedan
förälder
incheckning
3b64b7952d
5 ändrade filer med 389 tillägg och 10 borttagningar
  1. 10 8
      OpenPuck/OpenPuck.ino
  2. 5 2
      OpenPuck/config.h
  3. 3 0
      OpenPuck/controllers.cpp
  4. 345 0
      OpenPuck/mode_ps3.cpp
  5. 26 0
      OpenPuck/mode_ps3.h

+ 10 - 8
OpenPuck/OpenPuck.ino

@@ -45,8 +45,10 @@ using namespace Adafruit_LittleFS_Namespace;
 // puck composite (4 HID + WebUSB) exceeds the default 256 B config buffer
 static uint8_t g_usbCfgDesc[512];
 
-// Per-mode USB serial suffix (modes 1..8: X=xbox N=hori L=lizard P=swpro S=ps5 G=hidgyro Q=ps5game D=ds4game).
-static const char MODE_SUFFIX[] = { 'X', 'N', 'L', 'P', 'S', 'G', 'Q', 'D' };
+// Per-mode USB serial suffix (modes 1..9: X=xbox N=hori L=lizard P=swpro S=ps5 G=hidgyro Q=ps5game D=ds4game 3=ps3).
+static const char MODE_SUFFIX[] = {
+	'X', 'N', 'L', 'P', 'S', 'G', 'Q', 'D', '3'
+};
 // Fixed-interface flags captured at boot so usbReenumerate (dynamic mount, no reboot) replays them.
 static bool s_dynWantWebusb = false, s_dynWantWakeMouse = false;
 
@@ -62,11 +64,11 @@ void usbReenumerate(uint8_t k)
 	USBDevice.setConfigurationBuffer(g_usbCfgDesc, sizeof g_usbCfgDesc);
 	g_active->usbIdentity(); // clearConfiguration reset VID/PID/strings -- restore them
 	// serial carries the mounted count so the host invalidates its cached config descriptor on a change
-	snprintf(
-		g_usbSerial, sizeof g_usbSerial, "%s%c%u", g_unit,
-		MODE_SUFFIX[(g_usbMode >= 1 && g_usbMode <= 8) ? g_usbMode - 1 :
-								 0],
-		(unsigned)k);
+	snprintf(g_usbSerial, sizeof g_usbSerial, "%s%c%u", g_unit,
+		 MODE_SUFFIX[(g_usbMode >= 1 && g_usbMode <= MODE_MAX) ?
+				     g_usbMode - 1 :
+				     0],
+		 (unsigned)k);
 	USBDevice.setSerialDescriptor(g_usbSerial);
 	if (s_dynWantWakeMouse)
 		wakeHidAddInterface(); // HID instance 0
@@ -204,7 +206,7 @@ void setup()
 		"SWITCH(horipad)",     "LIZARD(puck kb/mouse)",
 		"SWITCH(pro+gyro)",    "PS5(dualsense)",
 		"HIDGYRO(ds4+motion)", "PS5(dualsense,game/clean)",
-		"DS4(ds4,game/clean)"
+		"DS4(ds4,game/clean)", "PS3(dualshock3+gyro)"
 	};
 	Serial.printf("# copycat up: unit=%s board=%s, mode=%s\n", g_unit,
 		      g_board,

+ 5 - 2
OpenPuck/config.h

@@ -37,14 +37,16 @@
 #define MODE_PS5_GAME 7
 // DS4, CLEAN single-HID (no wake/WebUSB) for game PlayStation classification
 #define MODE_DS4_GAME 8
-#define MODE_MAX 8
+// DualShock 3 (054C:0289) + gyro + haptics -- CLEAN single-HID for PS3 console and DS3 drivers
+#define MODE_PS3 9
+#define MODE_MAX 9
 
 // The two "game" personalities drop the wake-mouse + WebUSB interfaces so the device is a genuine single-HID PS
 // controller (some PC games -- e.g. Fortnite/UE GameInput -- refuse PS classification when extra interfaces are
 // present). Cost: no config panel / host-wake while in these modes; chord back to Steam (back4 + A) for the panel.
 static inline bool modeIsCleanPS(uint8_t m)
 {
-	return m == MODE_PS5_GAME || m == MODE_DS4_GAME;
+	return m == MODE_PS5_GAME || m == MODE_DS4_GAME || m == MODE_PS3;
 }
 
 static inline bool modeIsPuck(uint8_t m)
@@ -76,6 +78,7 @@ static inline uint8_t etypeForMode(uint8_t m)
 		return ET_SWITCH;
 	case MODE_HIDGYRO:
 	case MODE_DS4_GAME:
+	case MODE_PS3:
 		return ET_DS4;
 	case MODE_PS5:
 	case MODE_PS5_GAME:

+ 3 - 0
OpenPuck/controllers.cpp

@@ -6,6 +6,7 @@
 #include "mode_switch_pro.h"
 #include "mode_ps5.h"
 #include "mode_hidgyro.h"
+#include "mode_ps3.h"
 
 IController *g_active = nullptr;
 
@@ -36,6 +37,8 @@ IController *controllerFor(uint8_t mode)
 
 		// same DS4 controller; setup() drops wake/WebUSB for clean enum
 		return &g_hidGyroCtl;
+	case MODE_PS3:
+		return &g_ps3Ctl;
 	default:
 		return &g_steamPuck;
 	}

+ 345 - 0
OpenPuck/mode_ps3.cpp

@@ -0,0 +1,345 @@
+#include "mode_ps3.h"
+#include "triton.h"
+#include "gamepad_util.h"
+#include "config.h"
+#include "haptics.h"
+#include "bonds.h"
+#include "usb_mount.h"
+#include <Adafruit_TinyUSB.h>
+#include <Arduino.h>
+#include <string.h>
+
+Ps3Controller g_ps3Ctl;
+
+// DS3 HID report descriptor. Input report 0x01 = 48 bytes (48 payload after report ID).
+// Layout mirrors the real DualShock 3 byte-for-byte so the PS3 console and hid-sony/
+// hid-playstation drivers parse it correctly:
+//   [0-1]   reserved (constant)
+//   [2]     buttons byte 1: Select L3 R3 Start Up Right Down Left
+//   [3]     buttons byte 2: L2 R2 L1 R1 Triangle Circle Cross Square
+//   [4]     buttons byte 3: PS (bit 0), padding (bits 1-7)
+//   [5]     reserved (constant)
+//   [6-9]   LX LY RX RY analog sticks (0x80 center)
+//   [10-13] reserved
+//   [14-25] analog button pressures (Up Right Down Left L2 R2 L1 R1 Tri Cir X Sq)
+//   [26-27] reserved
+//   [28]    status byte (0x02 = USB connected)
+//   [29-39] reserved
+//   [40-41] AccX big-endian uint16 (center 512)
+//   [42-43] AccY big-endian uint16 (center 512)
+//   [44-45] AccZ big-endian uint16 (center 512)
+//   [46-47] GyroZ big-endian uint16 (center 512, yaw axis)
+static const uint8_t DS3_HID_DESC[] = {
+	0x05, 0x01, // Usage Page (Generic Desktop)
+	0x09, 0x04, // Usage (Joystick)
+	0xA1, 0x01, // Collection (Application)
+	0xA1, 0x02, //   Collection (Logical)
+	0x85, 0x01, //   Report ID (1)
+
+	// bytes 0-1: 2 constant (reserved) bytes
+	0x75, 0x08, //   Report Size (8)
+	0x95, 0x02, //   Report Count (2)
+	0x15, 0x00, //   Logical Min (0)
+	0x26, 0xFF, 0x00, //   Logical Max (255)
+	0x81, 0x03, //   Input (Const, Var, Abs)
+
+	// bytes 2-4 bits 0-18: 19 digital buttons
+	0x75, 0x01, //   Report Size (1)
+	0x95, 0x13, //   Report Count (19)
+	0x15, 0x00, //   Logical Min (0)
+	0x25, 0x01, //   Logical Max (1)
+	0x35, 0x00, //   Physical Min (0)
+	0x45, 0x01, //   Physical Max (1)
+	0x05, 0x09, //   Usage Page (Button)
+	0x19, 0x01, //   Usage Min (Button 1)
+	0x29, 0x13, //   Usage Max (Button 19)
+	0x81, 0x02, //   Input (Data, Var, Abs)
+
+	// byte 4 bits 3-7: 5 padding bits (complete the byte started by buttons 17-19)
+	0x75, 0x01, //   Report Size (1)
+	0x95, 0x05, //   Report Count (5)
+	0x81, 0x03, //   Input (Const, Var, Abs)
+
+	// byte 5: 1 reserved constant byte
+	0x75, 0x08, //   Report Size (8)
+	0x95, 0x01, //   Report Count (1)
+	0x81, 0x03, //   Input (Const, Var, Abs)
+
+	// bytes 6-9: LX LY RX RY (4 analog stick axes)
+	0x75, 0x08, //   Report Size (8)
+	0x95, 0x04, //   Report Count (4)
+	0x15, 0x00, //   Logical Min (0)
+	0x26, 0xFF, 0x00, //   Logical Max (255)
+	0x05, 0x01, //   Usage Page (Generic Desktop)
+	0x09, 0x01, //   Usage (Pointer)
+	0xA1, 0x00, //   Collection (Physical)
+	0x09, 0x30, //     Usage (X)
+	0x09, 0x31, //     Usage (Y)
+	0x09, 0x32, //     Usage (Z)
+	0x09, 0x35, //     Usage (Rz)
+	0x81, 0x02, //     Input (Data, Var, Abs)
+	0xC0, //   End Collection (Physical)
+
+	// bytes 10-47: 38 bytes (reserved, pressures, status, IMU)
+	0x75, 0x08, //   Report Size (8)
+	0x95, 0x26, //   Report Count (38)
+	0x15, 0x00, //   Logical Min (0)
+	0x26, 0xFF, 0x00, //   Logical Max (255)
+	0x09, 0x01, //   Usage (Pointer)
+	0x81, 0x02, //   Input (Data, Var, Abs)
+
+	// output report 0x01: 48 bytes (rumble + LED control from PS3)
+	0x75, 0x08, //   Report Size (8)
+	0x95, 0x30, //   Report Count (48)
+	0x15, 0x00, //   Logical Min (0)
+	0x26, 0xFF, 0x00, //   Logical Max (255)
+	0x09, 0x01, //   Usage (Pointer)
+	0x91, 0x02, //   Output (Data, Var, Abs)
+
+	0xC0, // End Collection (Logical)
+	0xC0, // End Collection (Application)
+};
+
+// One HID slot -- PS3 console expects one gamepad per USB port.
+static Adafruit_USBD_HID g_ds3;
+static unsigned long g_ds3LastMs = 0;
+
+// Per-slot MAC: OUI 001BDC is Sony's; last byte distinguishes slot.
+static const uint8_t DS3_MAC_BASE[5] = { 0x00, 0x1B, 0xDC, 0x4F, 0x55 };
+static uint8_t g_ds3Mac[6];
+static bool g_ds3MacInit = false;
+static void initDs3Mac()
+{
+	if (g_ds3MacInit)
+		return;
+	memcpy(g_ds3Mac, DS3_MAC_BASE, 5);
+	g_ds3Mac[5] = 0x70;
+	g_ds3MacInit = true;
+}
+
+// Map signed SC2 int16 IMU value to DS3 unsigned big-endian 10-bit (center 512).
+// The SC2 IMU outputs int16 values; scale >>6 maps ±2048 to ±32 around 512,
+// or ±32768 to ±512. Larger motions saturate at 0 / 1023. The PS3 kernel
+// driver applies its own calibration so the exact divisor is not critical; this
+// produces a usable live signal. Tune by adjusting the shift if the range feels
+// too narrow or too wide on real hardware.
+static uint16_t imuToDs3(int16_t v)
+{
+	int32_t out = 512 + ((int32_t)v >> 6);
+	if (out < 0)
+		out = 0;
+	if (out > 1023)
+		out = 1023;
+	return (uint16_t)out;
+}
+
+// GET_FEATURE handler. The PS3 reads 0xF2 (device info/MAC) and 0xF5 (connection
+// status) during the controller-enable handshake. TinyUSB strips the report ID and
+// passes us the payload buffer; we return the number of payload bytes to send.
+static uint16_t ds3GetReport(uint8_t rid, hid_report_type_t type, uint8_t *buf,
+			     uint16_t reqlen)
+{
+	if (type != HID_REPORT_TYPE_FEATURE || !buf || reqlen == 0)
+		return 0;
+	memset(buf, 0, reqlen);
+	switch (rid) {
+	// Device info / Bluetooth MAC (17 bytes total = 1 report ID + 16 payload).
+	// The PS3 reads this to identify and pair the controller via Bluetooth.
+	// payload[0] = 0x00 (status), payload[1..6] = controller BT MAC address.
+	case 0xF2:
+		if (reqlen < 16)
+			return 0;
+		memcpy(buf + 1, g_ds3Mac, 6);
+		return 16;
+	// Connection status (8 bytes total = 1 report ID + 7 payload).
+	// payload[0] = 0x10 signals USB-connected state to the PS3.
+	case 0xF5:
+		if (reqlen < 7)
+			return 0;
+		buf[0] = 0x10;
+		return 7;
+	default:
+		return 0;
+	}
+}
+
+// SET_REPORT handler. Receives rumble + LED output report (id 0x01) from the PS3
+// and feature 0xF4 (enable handshake). DS3 output layout (bytes after report ID):
+//   [1]  right (HFR) motor duration  (0xFF = run indefinitely)
+//   [2]  right motor on/off          (>0 means on; binary -- HFR has no speed control)
+//   [3]  left  (LFR) motor duration  (0xFF = run indefinitely)
+//   [4]  left  motor power           (0x00-0xFF)
+static void ds3SetReport(uint8_t rid, hid_report_type_t type, uint8_t const *b,
+			 uint16_t n)
+{
+	// Feature 0xF4 is the PS3's "enable SIXAXIS" command; accept silently.
+	if (type == HID_REPORT_TYPE_FEATURE)
+		return;
+	if (type != HID_REPORT_TYPE_OUTPUT || n < 5)
+		return;
+	// For output, TinyUSB may deliver the report ID in rid or in b[0].
+	const uint8_t *p;
+	uint16_t pn;
+	if (rid == 0) {
+		if (b[0] != 0x01 || n < 6)
+			return;
+		p = b + 1;
+		pn = (uint16_t)(n - 1);
+	} else {
+		if (rid != 0x01)
+			return;
+		p = b;
+		pn = n;
+	}
+	if (pn < 5)
+		return;
+	int bond = (g_usbMountCount > 0) ? g_usbToBond[0] : -1;
+	if (bond < 0)
+		return;
+	// p[4] = left (LFR) motor power, p[2] > 0 means right (HFR) motor on.
+	hapticSteamRumble((uint16_t)p[4] * 257u,
+			  (uint16_t)(p[2] > 0 ? 255 : 0) * 257u, (uint8_t)bond);
+}
+
+static void ds3Build(uint8_t slot, uint8_t out[48])
+{
+	uint32_t b = psButtonsFromSteam(g_in[slot].buttons);
+	bool l2 = (g_in[slot].lt > SW_TRIG_ON) || (b & TB_L2);
+	bool r2 = (g_in[slot].rt > SW_TRIG_ON) || (b & TB_R2);
+	memset(out, 0, 48);
+
+	// bytes 0-1: reserved (stay zero from memset)
+
+	// byte 2: Select L3 R3 Start Up Right Down Left
+	out[2] = ((b & TB_VIEW) ? 0x01 : 0) | ((b & TB_L3) ? 0x02 : 0) |
+		 ((b & TB_R3) ? 0x04 : 0) | ((b & TB_MENU) ? 0x08 : 0) |
+		 ((b & TB_DUP) ? 0x10 : 0) | ((b & TB_DRT) ? 0x20 : 0) |
+		 ((b & TB_DDN) ? 0x40 : 0) | ((b & TB_DLF) ? 0x80 : 0);
+
+	// byte 3: L2 R2 L1 R1 Triangle Circle Cross Square
+	// g_abSwap swaps A<->B and X<->Y (Nintendo vs PlayStation face layout).
+	if (g_abSwap) {
+		out[3] = (l2 ? 0x01 : 0) | (r2 ? 0x02 : 0) |
+			 ((b & TB_LB) ? 0x04 : 0) | ((b & TB_RB) ? 0x08 : 0) |
+			 ((b & TB_A) ? 0x10 : 0) | // Triangle
+			 ((b & TB_B) ? 0x20 : 0) | // Circle
+			 ((b & TB_X) ? 0x40 : 0) | // Cross
+			 ((b & TB_Y) ? 0x80 : 0); // Square
+	} else {
+		out[3] = (l2 ? 0x01 : 0) | (r2 ? 0x02 : 0) |
+			 ((b & TB_LB) ? 0x04 : 0) | ((b & TB_RB) ? 0x08 : 0) |
+			 ((b & TB_Y) ? 0x10 : 0) | // Triangle
+			 ((b & TB_B) ? 0x20 : 0) | // Circle
+			 ((b & TB_A) ? 0x40 : 0) | // Cross
+			 ((b & TB_X) ? 0x80 : 0); // Square
+	}
+
+	// byte 4: PS button (bit 0); bits 1-7 are the 5-bit HID padding (stay zero)
+	out[4] = (b & TB_STEAM) ? 0x01 : 0;
+
+	// byte 5: reserved (stays zero)
+
+	// bytes 6-9: analog sticks
+	out[6] = swStick(g_in[slot].lx, false);
+	out[7] = swStick(g_in[slot].ly, true);
+	out[8] = swStick(g_in[slot].rx, false);
+	out[9] = swStick(g_in[slot].ry, true);
+
+	// bytes 10-13: reserved
+
+	// bytes 14-25: analog button pressures (0xFF when pressed, 0x00 released)
+	out[14] = (b & TB_DUP) ? 0xFF : 0;
+	out[15] = (b & TB_DRT) ? 0xFF : 0;
+	out[16] = (b & TB_DDN) ? 0xFF : 0;
+	out[17] = (b & TB_DLF) ? 0xFF : 0;
+	out[18] = g_in[slot].lt; // L2 analog
+	out[19] = g_in[slot].rt; // R2 analog
+	out[20] = (b & TB_LB) ? 0xFF : 0; // L1
+	out[21] = (b & TB_RB) ? 0xFF : 0; // R1
+	if (g_abSwap) {
+		out[22] = (b & TB_A) ? 0xFF : 0; // Triangle
+		out[23] = (b & TB_B) ? 0xFF : 0; // Circle
+		out[24] = (b & TB_X) ? 0xFF : 0; // Cross
+		out[25] = (b & TB_Y) ? 0xFF : 0; // Square
+	} else {
+		out[22] = (b & TB_Y) ? 0xFF : 0; // Triangle
+		out[23] = (b & TB_B) ? 0xFF : 0; // Circle
+		out[24] = (b & TB_A) ? 0xFF : 0; // Cross
+		out[25] = (b & TB_X) ? 0xFF : 0; // Square
+	}
+
+	// bytes 26-27: reserved
+	// byte 28: status -- 0x02 = USB connected
+	out[28] = 0x02;
+	// bytes 29-39: reserved
+
+	// bytes 40-47: IMU. SC2 int16 (center 0) -> DS3 big-endian uint16 (center 512).
+	// AccX/Y/Z are the three accelerometer axes. GyroZ is the single yaw axis of
+	// the DS3 (the SIXAXIS/DS3 has only one gyro axis, unlike the DualSense/DS4).
+	uint16_t accX = imuToDs3(g_in[slot].ax);
+	uint16_t accY = imuToDs3(g_in[slot].ay);
+	uint16_t accZ = imuToDs3(g_in[slot].az);
+	uint16_t gyrZ = imuToDs3(g_in[slot].gz); // yaw axis
+	out[40] = (uint8_t)(accX >> 8);
+	out[41] = (uint8_t)(accX & 0xFF);
+	out[42] = (uint8_t)(accY >> 8);
+	out[43] = (uint8_t)(accY & 0xFF);
+	out[44] = (uint8_t)(accZ >> 8);
+	out[45] = (uint8_t)(accZ & 0xFF);
+	out[46] = (uint8_t)(gyrZ >> 8);
+	out[47] = (uint8_t)(gyrZ & 0xFF);
+}
+
+void Ps3Controller::begin()
+{
+}
+
+// maxSlots = 1: the PS3 console expects one gamepad per USB port. Even on PC, a single
+// DS3 interface is the standard configuration for this personality.
+uint8_t Ps3Controller::maxSlots() const
+{
+	return 1;
+}
+
+void Ps3Controller::usbIdentity()
+{
+	// DualShock 3 (PID 0x0289 = with vibration, vs SIXAXIS 0x0268 without).
+	// Using DS3 so the PS3 sends rumble output reports.
+	USBDevice.setID(0x054C, 0x0289);
+	USBDevice.setDeviceVersion(0x0100);
+	USBDevice.setManufacturerDescriptor("Sony Computer Entertainment");
+	USBDevice.setProductDescriptor("PLAYSTATION(R)3 Controller");
+}
+
+void Ps3Controller::beginPool()
+{
+	initDs3Mac();
+	g_ds3.enableOutEndpoint(true);
+	g_ds3.setReportCallback(ds3GetReport, ds3SetReport);
+	g_ds3.setReportDescriptor(DS3_HID_DESC, sizeof DS3_HID_DESC);
+	g_ds3.setPollInterval(1);
+	g_ds3.begin();
+}
+
+void Ps3Controller::mountSlots(uint8_t k)
+{
+	if (k > 0)
+		USBDevice.addInterface(g_ds3);
+}
+
+void Ps3Controller::task()
+{
+	if (g_usbMountCount < 1)
+		return;
+	if (!g_ds3.ready())
+		return;
+	if (millis() - g_ds3LastMs < USB_STREAM_MS)
+		return;
+	int bond = g_usbToBond[0];
+	if (bond < 0)
+		return;
+	g_ds3LastMs = millis();
+	uint8_t p[48];
+	ds3Build((uint8_t)bond, p);
+	g_ds3.sendReport(0x01, p, sizeof p);
+}

+ 26 - 0
OpenPuck/mode_ps3.h

@@ -0,0 +1,26 @@
+// mode_ps3.h -- Sony DualShock 3 personality (MODE_PS3): 054C:0289 + gyro + haptics.
+//
+// Presents the device as a DualShock 3 (PID 0x0289, with vibration) so it enumerates
+// cleanly on a PS3 console and PS3-aware PC drivers. One HID slot (maxSlots=1): the PS3
+// expects a single gamepad per USB port. Input streamed at ~250Hz from task(): sticks,
+// digital buttons, analog button pressures, and the SC2 IMU mapped to the DS3's 3-axis
+// accelerometer (unsigned big-endian, center 512) + single-axis gyro (yaw, same format).
+// Haptic relay handles the PS3's rumble OUTPUT report (report 0x01, bytes 2 and 4).
+// Feature reports 0xF2 and 0xF5 satisfy the PS3's DS3 identification handshake.
+#pragma once
+#include "controllers.h"
+
+class Ps3Controller : public IController {
+    public:
+	void begin() override;
+	void task() override;
+	bool dynamicMount() const override
+	{
+		return true;
+	}
+	uint8_t maxSlots() const override;
+	void usbIdentity() override;
+	void beginPool() override;
+	void mountSlots(uint8_t k) override;
+};
+extern Ps3Controller g_ps3Ctl;