From d73996b0d50254f4d0a6d09c4240be592a03205e Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sun, 21 Jun 2026 20:38:19 -0400 Subject: [PATCH] feat(gamepads): add DualShock4 --- README.md | 21 +- src/core/profiles.cpp | 1427 ++++++++++++++--- src/core/report.cpp | 251 ++- src/include/libvirtualhid/profiles.hpp | 21 + src/include/libvirtualhid/types.hpp | 2 + src/platform/linux/uhid_backend.cpp | 233 ++- src/platform/windows/control_protocol.hpp | 2 + .../windows/shared/lvh_windows_protocol.h | 5 + .../fixtures/linux_backend_test_hooks.hpp | 39 + tests/fixtures/linux_backend_test_hooks.cpp | 137 +- tests/unit/test_linux_backend.cpp | 21 + tests/unit/test_linux_consumers.cpp | 44 +- tests/unit/test_profiles.cpp | 19 + tests/unit/test_report.cpp | 129 +- tests/unit/test_windows_protocol.cpp | 4 + 15 files changed, 2050 insertions(+), 305 deletions(-) diff --git a/README.md b/README.md index d1fe1c5..3d009af 100644 --- a/README.md +++ b/README.md @@ -349,9 +349,9 @@ the requirements expressed in terms that apply to other consumers: streaming hosts can preserve stable controller lifecycles across arrival, update, feedback, and removal events. - [x] Built-in profiles should cover common streaming controller choices: - automatic selection, Xbox One-style, DualSense-style, and Switch Pro-style - devices. Xbox 360 can remain useful as a compatibility profile and test - target. + automatic selection, Xbox One-style, DualShock 4-style, DualSense-style, and + Switch Pro-style devices. Xbox 360 can remain useful as a compatibility + profile and test target. - [x] Controller metadata must be rich enough for streaming-host selection rules: client controller type, motion sensor capability, touchpad capability, RGB LED support, battery state, and per-controller identity data. @@ -434,8 +434,8 @@ third-party/googletest/ GoogleTest submodule - [x] Add CI using the `libdisplaydevice` workflow pattern for Linux GCC, Linux Clang, macOS, Windows MinGW/UCRT64, and Windows MSVC configure/build/test coverage. -- [x] Add descriptor/profile models for at least Xbox 360, Xbox Series, DualSense, - and a generic HID gamepad. +- [x] Add descriptor/profile models for at least Xbox 360, Xbox Series, + DualShock 4, DualSense, and a generic HID gamepad. - [x] Add unit tests for state normalization and HID report packing. - [x] Add a streaming-host-oriented example or adapter test that exercises controller arrival, state updates, output feedback, and removal without @@ -448,8 +448,9 @@ third-party/googletest/ GoogleTest submodule - [x] Support output report callbacks for rumble and profile-specific feedback. - [x] Add X11/XTest fallback support for keyboard and mouse only. - [x] Add examples and integration tests that validate virtual device visibility - through SDL2 for generic gamepad input, DualSense USB input, and DualSense - Bluetooth controller discovery, plus libinput for keyboard/mouse. + through SDL2 for generic gamepad input, DualShock 4 USB input, DualShock 4 + Bluetooth controller discovery, DualSense USB input, and DualSense Bluetooth + controller discovery, plus libinput for keyboard/mouse. - [x] Document required Linux permissions and sample udev rules. ### Phase 2B: Linux inputtino Parity @@ -467,6 +468,11 @@ third-party/googletest/ GoogleTest submodule into consumers. - [x] Parse DualSense output reports into rumble, RGB LED, adaptive trigger, and raw-report callbacks. +- [x] Add DualShock 4 USB and Bluetooth profiles with descriptor-driven input + reports, touchpad click, touch contacts, motion sensors, battery state, + lightbar feedback, rumble callbacks, Bluetooth CRC handling, GET_REPORT + replies, stable MAC identity, periodic reports, and generated sensor + timestamps. - [x] Expose created device nodes and sysfs paths through the platform-neutral public API. - [x] Add configurable keyboard auto-repeat for held keys. @@ -476,6 +482,7 @@ third-party/googletest/ GoogleTest submodule gamepad path in this library; if one is added later, it must implement Linux force-feedback upload, erase, playback, and gain handling. - [x] Expand Linux consumer tests so SDL2 validates generic joystick input, + DualShock 4 USB controller input, DualShock 4 Bluetooth controller discovery, DualSense USB controller input, and DualSense Bluetooth controller discovery, and libinput validates keyboard, mouse, touchscreen, trackpad, and pen tablet events. diff --git a/src/core/profiles.cpp b/src/core/profiles.cpp index dee08a4..25acd2b 100644 --- a/src/core/profiles.cpp +++ b/src/core/profiles.cpp @@ -19,6 +19,14 @@ namespace lvh::profiles { constexpr std::size_t common_output_report_size = 5; + constexpr std::size_t dualshock4_usb_input_report_size = 64; + + constexpr std::size_t dualshock4_usb_output_report_size = 32; + + constexpr std::size_t dualshock4_bluetooth_input_report_size = 78; + + constexpr std::size_t dualshock4_bluetooth_output_report_size = 78; + constexpr std::size_t dualsense_usb_input_report_size = 64; constexpr std::size_t dualsense_usb_output_report_size = 48; @@ -151,287 +159,939 @@ namespace lvh::profiles { return descriptor; } - std::vector make_dualsense_usb_report_descriptor() { - // DualSense USB descriptor data is derived from the public reverse-engineered descriptor used by inputtino. + std::vector make_playstation_common_gamepad_descriptor_prefix(std::uint8_t report_id) { return { + // Usage Page (Generic Desktop) 0x05, 0x01, + // Usage (Game Pad) 0x09, 0x05, + // Collection (Application) 0xA1, 0x01, + // Report ID 0x85, - 0x01, + report_id, + // Usage (X) 0x09, 0x30, + // Usage (Y) 0x09, 0x31, + // Usage (Z) 0x09, 0x32, + // Usage (Rz) 0x09, 0x35, - 0x09, - 0x33, - 0x09, - 0x34, + // Logical Minimum (0) 0x15, 0x00, + // Logical Maximum (255) 0x26, 0xFF, 0x00, + // Report Size (8) 0x75, 0x08, + // Report Count (4) 0x95, - 0x06, - 0x81, - 0x02, - 0x06, - 0x00, - 0xFF, - 0x09, - 0x20, - 0x95, - 0x01, + 0x04, + // Input (Data,Var,Abs) 0x81, 0x02, - 0x05, - 0x01, + // Usage (Hat switch) 0x09, 0x39, + // Logical Minimum (0) 0x15, 0x00, + // Logical Maximum (7) 0x25, 0x07, + // Physical Minimum (0) 0x35, 0x00, + // Physical Maximum (315) 0x46, 0x3B, 0x01, + // Unit (Degrees) 0x65, 0x14, + // Report Size (4) 0x75, 0x04, + // Report Count (1) 0x95, 0x01, + // Input (Data,Var,Abs,Null) 0x81, 0x42, + // Unit (None) 0x65, 0x00, + // Usage Page (Button) 0x05, 0x09, + // Usage Minimum (Button 1) 0x19, 0x01, + // Usage Maximum (Button 14) 0x29, - 0x0F, + 0x0E, + // Logical Minimum (0) 0x15, 0x00, + // Logical Maximum (1) 0x25, 0x01, + // Report Size (1) 0x75, 0x01, + // Report Count (14) 0x95, - 0x0F, + 0x0E, + // Input (Data,Var,Abs) 0x81, 0x02, - 0x06, - 0x00, - 0xFF, + }; + } + + std::vector make_dualshock4_usb_report_descriptor() { + // DualShock 4 USB descriptor data is derived from the public descriptor used by WinUHid. + auto descriptor = make_playstation_common_gamepad_descriptor_prefix(0x01); + descriptor.insert( + descriptor.end(), + { + // Usage Page (Vendor Defined) + 0x06, + 0x00, + 0xFF, + // Usage (Vendor Usage 0x20) + 0x09, + 0x20, + // Report Size (6) + 0x75, + 0x06, + // Report Count (1) + 0x95, + 0x01, + // Logical Minimum (0) + 0x15, + 0x00, + // Logical Maximum (127) + 0x25, + 0x7F, + // Input (Data,Var,Abs) + 0x81, + 0x02, + // Usage Page (Generic Desktop) + 0x05, + 0x01, + // Usage (Rx) + 0x09, + 0x33, + // Usage (Ry) + 0x09, + 0x34, + // Logical Minimum (0) + 0x15, + 0x00, + // Logical Maximum (255) + 0x26, + 0xFF, + 0x00, + // Report Size (8) + 0x75, + 0x08, + // Report Count (2) + 0x95, + 0x02, + // Input (Data,Var,Abs) + 0x81, + 0x02, + // Usage Page (Vendor Defined) + 0x06, + 0x00, + 0xFF, + // Usage (Vendor Usage 0x21) + 0x09, + 0x21, + // Report Count (54) + 0x95, + 0x36, + // Input (Data,Var,Abs) + 0x81, + 0x02, + // Report ID (5) + 0x85, + 0x05, + // Usage (Vendor Usage 0x22) + 0x09, + 0x22, + // Report Count (31) + 0x95, + 0x1F, + // Output (Data,Var,Abs) + 0x91, + 0x02, + // Report ID (4) + 0x85, + 0x04, + // Usage (Vendor Usage 0x23) + 0x09, + 0x23, + // Report Count (36) + 0x95, + 0x24, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (2) + 0x85, + 0x02, + // Usage (Vendor Usage 0x24) + 0x09, + 0x24, + // Report Count (36) + 0x95, + 0x24, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (8) + 0x85, + 0x08, + // Usage (Vendor Usage 0x25) + 0x09, + 0x25, + // Report Count (3) + 0x95, + 0x03, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (16) + 0x85, + 0x10, + // Usage (Vendor Usage 0x26) + 0x09, + 0x26, + // Report Count (4) + 0x95, + 0x04, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (17) + 0x85, + 0x11, + // Usage (Vendor Usage 0x27) + 0x09, + 0x27, + // Report Count (2) + 0x95, + 0x02, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (18) + 0x85, + 0x12, + // Usage Page (Vendor Defined 0xFF02) + 0x06, + 0x02, + 0xFF, + // Usage (Vendor Usage 0x21) + 0x09, + 0x21, + // Report Count (15) + 0x95, + 0x0F, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (19) + 0x85, + 0x13, + // Usage (Vendor Usage 0x22) + 0x09, + 0x22, + // Report Count (22) + 0x95, + 0x16, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (20) + 0x85, + 0x14, + // Usage Page (Vendor Defined 0xFF05) + 0x06, + 0x05, + 0xFF, + // Usage (Vendor Usage 0x20) + 0x09, + 0x20, + // Report Count (16) + 0x95, + 0x10, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (21) + 0x85, + 0x15, + // Usage (Vendor Usage 0x21) + 0x09, + 0x21, + // Report Count (44) + 0x95, + 0x2C, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Usage Page (Vendor Defined 0xFF80) + 0x06, + 0x80, + 0xFF, + // Report ID (128) + 0x85, + 0x80, + // Usage (Vendor Usage 0x20) + 0x09, + 0x20, + // Report Count (6) + 0x95, + 0x06, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (129) + 0x85, + 0x81, + // Usage (Vendor Usage 0x21) + 0x09, + 0x21, + // Report Count (6) + 0x95, + 0x06, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (130) + 0x85, + 0x82, + // Usage (Vendor Usage 0x22) + 0x09, + 0x22, + // Report Count (5) + 0x95, + 0x05, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (131) + 0x85, + 0x83, + // Usage (Vendor Usage 0x23) + 0x09, + 0x23, + // Report Count (1) + 0x95, + 0x01, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (132) + 0x85, + 0x84, + // Usage (Vendor Usage 0x24) + 0x09, + 0x24, + // Report Count (4) + 0x95, + 0x04, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (133) + 0x85, + 0x85, + // Usage (Vendor Usage 0x25) + 0x09, + 0x25, + // Report Count (6) + 0x95, + 0x06, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (134) + 0x85, + 0x86, + // Usage (Vendor Usage 0x26) + 0x09, + 0x26, + // Report Count (6) + 0x95, + 0x06, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (135) + 0x85, + 0x87, + // Usage (Vendor Usage 0x27) + 0x09, + 0x27, + // Report Count (35) + 0x95, + 0x23, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (136) + 0x85, + 0x88, + // Usage (Vendor Usage 0x28) + 0x09, + 0x28, + // Report Count (34) + 0x95, + 0x22, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (137) + 0x85, + 0x89, + // Usage (Vendor Usage 0x29) + 0x09, + 0x29, + // Report Count (2) + 0x95, + 0x02, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (144) + 0x85, + 0x90, + // Usage (Vendor Usage 0x30) + 0x09, + 0x30, + // Report Count (5) + 0x95, + 0x05, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (145) + 0x85, + 0x91, + // Usage (Vendor Usage 0x31) + 0x09, + 0x31, + // Report Count (3) + 0x95, + 0x03, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (146) + 0x85, + 0x92, + // Usage (Vendor Usage 0x32) + 0x09, + 0x32, + // Report Count (3) + 0x95, + 0x03, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (147) + 0x85, + 0x93, + // Usage (Vendor Usage 0x33) + 0x09, + 0x33, + // Report Count (12) + 0x95, + 0x0C, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (160) + 0x85, + 0xA0, + // Usage (Vendor Usage 0x40) + 0x09, + 0x40, + // Report Count (6) + 0x95, + 0x06, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (161) + 0x85, + 0xA1, + // Usage (Vendor Usage 0x41) + 0x09, + 0x41, + // Report Count (1) + 0x95, + 0x01, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (162) + 0x85, + 0xA2, + // Usage (Vendor Usage 0x42) + 0x09, + 0x42, + // Report Count (1) + 0x95, + 0x01, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (163) + 0x85, + 0xA3, + // Usage (Vendor Usage 0x43) + 0x09, + 0x43, + // Report Count (48) + 0x95, + 0x30, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (164) + 0x85, + 0xA4, + // Usage (Vendor Usage 0x44) + 0x09, + 0x44, + // Report Count (13) + 0x95, + 0x0D, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (165) + 0x85, + 0xA5, + // Usage (Vendor Usage 0x45) + 0x09, + 0x45, + // Report Count (21) + 0x95, + 0x15, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (166) + 0x85, + 0xA6, + // Usage (Vendor Usage 0x46) + 0x09, + 0x46, + // Report Count (21) + 0x95, + 0x15, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (240) + 0x85, + 0xF0, + // Usage (Vendor Usage 0x47) + 0x09, + 0x47, + // Report Count (63) + 0x95, + 0x3F, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (241) + 0x85, + 0xF1, + // Usage (Vendor Usage 0x48) + 0x09, + 0x48, + // Report Count (63) + 0x95, + 0x3F, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (242) + 0x85, + 0xF2, + // Usage (Vendor Usage 0x49) + 0x09, + 0x49, + // Report Count (15) + 0x95, + 0x0F, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (167) + 0x85, + 0xA7, + // Usage (Vendor Usage 0x4A) + 0x09, + 0x4A, + // Report Count (1) + 0x95, + 0x01, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (168) + 0x85, + 0xA8, + // Usage (Vendor Usage 0x4B) + 0x09, + 0x4B, + // Report Count (1) + 0x95, + 0x01, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (169) + 0x85, + 0xA9, + // Usage (Vendor Usage 0x4C) + 0x09, + 0x4C, + // Report Count (8) + 0x95, + 0x08, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (170) + 0x85, + 0xAA, + // Usage (Vendor Usage 0x4E) + 0x09, + 0x4E, + // Report Count (1) + 0x95, + 0x01, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (171) + 0x85, + 0xAB, + // Usage (Vendor Usage 0x4F) + 0x09, + 0x4F, + // Report Count (57) + 0x95, + 0x39, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (172) + 0x85, + 0xAC, + // Usage (Vendor Usage 0x50) + 0x09, + 0x50, + // Report Count (57) + 0x95, + 0x39, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (173) + 0x85, + 0xAD, + // Usage (Vendor Usage 0x51) + 0x09, + 0x51, + // Report Count (11) + 0x95, + 0x0B, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (174) + 0x85, + 0xAE, + // Usage (Vendor Usage 0x52) + 0x09, + 0x52, + // Report Count (1) + 0x95, + 0x01, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (175) + 0x85, + 0xAF, + // Usage (Vendor Usage 0x53) + 0x09, + 0x53, + // Report Count (2) + 0x95, + 0x02, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // Report ID (176) + 0x85, + 0xB0, + // Usage (Vendor Usage 0x54) + 0x09, + 0x54, + // Report Count (63) + 0x95, + 0x3F, + // Feature (Data,Var,Abs) + 0xB1, + 0x02, + // End Collection + 0xC0, + } + ); + + return descriptor; + } + + std::vector make_dualshock4_bluetooth_report_descriptor() { + // DualShock 4 Bluetooth report framing follows the Linux hid-playstation DS4 layout. + return { + // Usage Page (Generic Desktop) + 0x05, + 0x01, + // Usage (Game Pad) 0x09, - 0x21, - 0x95, - 0x0D, - 0x81, - 0x02, + 0x05, + // Collection (Application) + 0xA1, + 0x01, + // Report ID (17) + 0x85, + 0x11, + // Usage Page (Vendor Defined) 0x06, 0x00, 0xFF, + // Usage (Vendor Usage 0x20) 0x09, - 0x22, + 0x20, + // Logical Minimum (0) 0x15, 0x00, + // Logical Maximum (255) 0x26, 0xFF, 0x00, + // Report Size (8) 0x75, 0x08, + // Report Count (2) 0x95, - 0x34, - 0x81, 0x02, - 0x85, - 0x02, - 0x09, - 0x23, - 0x95, - 0x2F, - 0x91, + // Input (Data,Var,Abs) + 0x81, 0x02, - 0x85, + // Usage Page (Generic Desktop) 0x05, + 0x01, + // Usage (X) 0x09, - 0x33, - 0x95, - 0x28, - 0xB1, - 0x02, - 0x85, - 0x08, + 0x30, + // Usage (Y) 0x09, - 0x34, - 0x95, - 0x2F, - 0xB1, - 0x02, - 0x85, + 0x31, + // Usage (Z) 0x09, + 0x32, + // Usage (Rz) 0x09, - 0x24, + 0x35, + // Report Size (8) + 0x75, + 0x08, + // Report Count (4) 0x95, - 0x13, - 0xB1, + 0x04, + // Input (Data,Var,Abs) + 0x81, 0x02, - 0x85, - 0x0A, + // Usage (Hat switch) 0x09, + 0x39, + // Logical Minimum (0) + 0x15, + 0x00, + // Logical Maximum (7) 0x25, - 0x95, - 0x1A, - 0xB1, - 0x02, - 0x85, - 0x20, - 0x09, - 0x26, - 0x95, - 0x3F, - 0xB1, - 0x02, - 0x85, - 0x21, - 0x09, - 0x27, - 0x95, + 0x07, + // Physical Minimum (0) + 0x35, + 0x00, + // Physical Maximum (315) + 0x46, + 0x3B, + 0x01, + // Unit (Degrees) + 0x65, + 0x14, + // Report Size (4) + 0x75, 0x04, - 0xB1, - 0x02, - 0x85, - 0x22, - 0x09, - 0x40, + // Report Count (1) 0x95, - 0x3F, - 0xB1, - 0x02, - 0x85, - 0x80, - 0x09, - 0x28, - 0x95, - 0x3F, - 0xB1, - 0x02, - 0x85, + 0x01, + // Input (Data,Var,Abs,Null) 0x81, + 0x42, + // Unit (None) + 0x65, + 0x00, + // Usage Page (Button) + 0x05, 0x09, + // Usage Minimum (Button 1) + 0x19, + 0x01, + // Usage Maximum (Button 14) 0x29, + 0x0E, + // Logical Minimum (0) + 0x15, + 0x00, + // Logical Maximum (1) + 0x25, + 0x01, + // Report Size (1) + 0x75, + 0x01, + // Report Count (14) 0x95, - 0x3F, - 0xB1, - 0x02, - 0x85, - 0x82, - 0x09, - 0x2A, - 0x95, - 0x09, - 0xB1, + 0x0E, + // Input (Data,Var,Abs) + 0x81, 0x02, - 0x85, - 0x83, + // Usage Page (Vendor Defined) + 0x06, + 0x00, + 0xFF, + // Usage (Vendor Usage 0x21) 0x09, - 0x2B, + 0x21, + // Report Size (6) + 0x75, + 0x06, + // Report Count (1) 0x95, - 0x3F, - 0xB1, + 0x01, + // Input (Data,Var,Abs) + 0x81, 0x02, - 0x85, - 0x84, + // Usage Page (Generic Desktop) + 0x05, + 0x01, + // Usage (Rx) 0x09, - 0x2C, - 0x95, - 0x3F, - 0xB1, - 0x02, - 0x85, - 0x85, + 0x33, + // Usage (Ry) 0x09, - 0x2D, + 0x34, + // Report Size (8) + 0x75, + 0x08, + // Report Count (2) 0x95, 0x02, - 0xB1, - 0x02, - 0x85, - 0xA0, - 0x09, - 0x2E, - 0x95, - 0x01, - 0xB1, + // Input (Data,Var,Abs) + 0x81, 0x02, - 0x85, - 0xE0, + // Usage Page (Vendor Defined) + 0x06, + 0x00, + 0xFF, + // Usage (Vendor Usage 0x22) 0x09, - 0x2F, + 0x22, + // Report Count (66) 0x95, - 0x3F, - 0xB1, + 0x42, + // Input (Data,Var,Abs) + 0x81, 0x02, + // Report ID (17) 0x85, - 0xF0, + 0x11, + // Usage (Vendor Usage 0x23) 0x09, - 0x30, + 0x23, + // Report Count (77) 0x95, - 0x3F, - 0xB1, + 0x4D, + // Output (Data,Var,Abs) + 0x91, 0x02, + // Report ID (5) 0x85, - 0xF1, + 0x05, + // Usage (Vendor Usage 0x24) 0x09, - 0x31, + 0x24, + // Report Count (40) 0x95, - 0x3F, + 0x28, + // Feature (Data,Var,Abs) 0xB1, 0x02, + // Report ID (18) 0x85, - 0xF2, + 0x12, + // Usage (Vendor Usage 0x25) 0x09, - 0x32, + 0x25, + // Report Count (15) 0x95, 0x0F, + // Feature (Data,Var,Abs) 0xB1, 0x02, + // Report ID (163) 0x85, - 0xF4, + 0xA3, + // Usage (Vendor Usage 0x26) 0x09, - 0x35, - 0x95, - 0x3F, - 0xB1, - 0x02, - 0x85, - 0xF5, - 0x09, - 0x36, + 0x26, + // Report Count (48) 0x95, - 0x03, + 0x30, + // Feature (Data,Var,Abs) 0xB1, 0x02, + // End Collection 0xC0, }; } - std::vector make_dualsense_bluetooth_report_descriptor() { - // DualSense Bluetooth descriptor data is derived from the public reverse-engineered descriptor used by inputtino. + std::vector make_dualsense_usb_report_descriptor() { + // DualSense USB descriptor data is derived from the public reverse-engineered descriptor used by inputtino. return { 0x05, 0x01, @@ -449,6 +1109,10 @@ namespace lvh::profiles { 0x32, 0x09, 0x35, + 0x09, + 0x33, + 0x09, + 0x34, 0x15, 0x00, 0x26, @@ -457,9 +1121,20 @@ namespace lvh::profiles { 0x75, 0x08, 0x95, - 0x04, + 0x06, 0x81, 0x02, + 0x06, + 0x00, + 0xFF, + 0x09, + 0x20, + 0x95, + 0x01, + 0x81, + 0x02, + 0x05, + 0x01, 0x09, 0x39, 0x15, @@ -486,7 +1161,7 @@ namespace lvh::profiles { 0x19, 0x01, 0x29, - 0x0E, + 0x0F, 0x15, 0x00, 0x25, @@ -494,35 +1169,23 @@ namespace lvh::profiles { 0x75, 0x01, 0x95, - 0x0E, + 0x0F, 0x81, 0x02, - 0x75, 0x06, - 0x95, - 0x01, - 0x81, - 0x01, - 0x05, - 0x01, - 0x09, - 0x33, - 0x09, - 0x34, - 0x15, 0x00, - 0x26, 0xFF, - 0x00, - 0x75, - 0x08, + 0x09, + 0x21, 0x95, - 0x02, + 0x0D, 0x81, 0x02, 0x06, 0x00, 0xFF, + 0x09, + 0x22, 0x15, 0x00, 0x26, @@ -531,90 +1194,17 @@ namespace lvh::profiles { 0x75, 0x08, 0x95, - 0x4D, - 0x85, - 0x31, - 0x09, - 0x31, - 0x91, - 0x02, - 0x09, - 0x3B, + 0x34, 0x81, 0x02, 0x85, - 0x32, - 0x09, - 0x32, - 0x95, - 0x8D, - 0x91, 0x02, - 0x85, - 0x33, 0x09, - 0x33, + 0x23, 0x95, - 0xCD, - 0x91, - 0x02, - 0x85, - 0x34, - 0x09, - 0x34, - 0x96, - 0x0D, - 0x01, - 0x91, - 0x02, - 0x85, - 0x35, - 0x09, - 0x35, - 0x96, - 0x4D, - 0x01, - 0x91, - 0x02, - 0x85, - 0x36, - 0x09, - 0x36, - 0x96, - 0x8D, - 0x01, - 0x91, - 0x02, - 0x85, - 0x37, - 0x09, - 0x37, - 0x96, - 0xCD, - 0x01, - 0x91, - 0x02, - 0x85, - 0x38, - 0x09, - 0x38, - 0x96, - 0x0D, - 0x02, - 0x91, - 0x02, - 0x85, - 0x39, - 0x09, - 0x39, - 0x96, - 0x22, - 0x02, + 0x2F, 0x91, 0x02, - 0x06, - 0x80, - 0xFF, 0x85, 0x05, 0x09, @@ -640,6 +1230,14 @@ namespace lvh::profiles { 0xB1, 0x02, 0x85, + 0x0A, + 0x09, + 0x25, + 0x95, + 0x1A, + 0xB1, + 0x02, + 0x85, 0x20, 0x09, 0x26, @@ -648,6 +1246,14 @@ namespace lvh::profiles { 0xB1, 0x02, 0x85, + 0x21, + 0x09, + 0x27, + 0x95, + 0x04, + 0xB1, + 0x02, + 0x85, 0x22, 0x09, 0x40, @@ -688,6 +1294,46 @@ namespace lvh::profiles { 0xB1, 0x02, 0x85, + 0x84, + 0x09, + 0x2C, + 0x95, + 0x3F, + 0xB1, + 0x02, + 0x85, + 0x85, + 0x09, + 0x2D, + 0x95, + 0x02, + 0xB1, + 0x02, + 0x85, + 0xA0, + 0x09, + 0x2E, + 0x95, + 0x01, + 0xB1, + 0x02, + 0x85, + 0xE0, + 0x09, + 0x2F, + 0x95, + 0x3F, + 0xB1, + 0x02, + 0x85, + 0xF0, + 0x09, + 0x30, + 0x95, + 0x3F, + 0xB1, + 0x02, + 0x85, 0xF1, 0x09, 0x31, @@ -704,17 +1350,252 @@ namespace lvh::profiles { 0xB1, 0x02, 0x85, - 0xF0, + 0xF4, 0x09, - 0x30, + 0x35, 0x95, 0x3F, 0xB1, 0x02, + 0x85, + 0xF5, + 0x09, + 0x36, + 0x95, + 0x03, + 0xB1, + 0x02, 0xC0, }; } + std::vector make_dualsense_bluetooth_report_descriptor() { + // DualSense Bluetooth descriptor data is derived from the public reverse-engineered descriptor used by inputtino. + auto descriptor = make_playstation_common_gamepad_descriptor_prefix(0x01); + descriptor.insert( + descriptor.end(), + { + 0x75, + 0x06, + 0x95, + 0x01, + 0x81, + 0x01, + 0x05, + 0x01, + 0x09, + 0x33, + 0x09, + 0x34, + 0x15, + 0x00, + 0x26, + 0xFF, + 0x00, + 0x75, + 0x08, + 0x95, + 0x02, + 0x81, + 0x02, + 0x06, + 0x00, + 0xFF, + 0x15, + 0x00, + 0x26, + 0xFF, + 0x00, + 0x75, + 0x08, + 0x95, + 0x4D, + 0x85, + 0x31, + 0x09, + 0x31, + 0x91, + 0x02, + 0x09, + 0x3B, + 0x81, + 0x02, + 0x85, + 0x32, + 0x09, + 0x32, + 0x95, + 0x8D, + 0x91, + 0x02, + 0x85, + 0x33, + 0x09, + 0x33, + 0x95, + 0xCD, + 0x91, + 0x02, + 0x85, + 0x34, + 0x09, + 0x34, + 0x96, + 0x0D, + 0x01, + 0x91, + 0x02, + 0x85, + 0x35, + 0x09, + 0x35, + 0x96, + 0x4D, + 0x01, + 0x91, + 0x02, + 0x85, + 0x36, + 0x09, + 0x36, + 0x96, + 0x8D, + 0x01, + 0x91, + 0x02, + 0x85, + 0x37, + 0x09, + 0x37, + 0x96, + 0xCD, + 0x01, + 0x91, + 0x02, + 0x85, + 0x38, + 0x09, + 0x38, + 0x96, + 0x0D, + 0x02, + 0x91, + 0x02, + 0x85, + 0x39, + 0x09, + 0x39, + 0x96, + 0x22, + 0x02, + 0x91, + 0x02, + 0x06, + 0x80, + 0xFF, + 0x85, + 0x05, + 0x09, + 0x33, + 0x95, + 0x28, + 0xB1, + 0x02, + 0x85, + 0x08, + 0x09, + 0x34, + 0x95, + 0x2F, + 0xB1, + 0x02, + 0x85, + 0x09, + 0x09, + 0x24, + 0x95, + 0x13, + 0xB1, + 0x02, + 0x85, + 0x20, + 0x09, + 0x26, + 0x95, + 0x3F, + 0xB1, + 0x02, + 0x85, + 0x22, + 0x09, + 0x40, + 0x95, + 0x3F, + 0xB1, + 0x02, + 0x85, + 0x80, + 0x09, + 0x28, + 0x95, + 0x3F, + 0xB1, + 0x02, + 0x85, + 0x81, + 0x09, + 0x29, + 0x95, + 0x3F, + 0xB1, + 0x02, + 0x85, + 0x82, + 0x09, + 0x2A, + 0x95, + 0x09, + 0xB1, + 0x02, + 0x85, + 0x83, + 0x09, + 0x2B, + 0x95, + 0x3F, + 0xB1, + 0x02, + 0x85, + 0xF1, + 0x09, + 0x31, + 0x95, + 0x3F, + 0xB1, + 0x02, + 0x85, + 0xF2, + 0x09, + 0x32, + 0x95, + 0x0F, + 0xB1, + 0x02, + 0x85, + 0xF0, + 0x09, + 0x30, + 0x95, + 0x3F, + 0xB1, + 0x02, + 0xC0, + } + ); + + return descriptor; + } + DeviceProfile make_gamepad_profile( GamepadProfileKind kind, std::string name, @@ -742,6 +1623,33 @@ namespace lvh::profiles { return profile; } + DeviceProfile make_dualshock4_profile(BusType bus_type) { + DeviceProfile profile; + profile.device_type = DeviceType::gamepad; + profile.gamepad_kind = GamepadProfileKind::dualshock4; + profile.bus_type = bus_type; + profile.vendor_id = 0x054C; + profile.product_id = 0x05C4; + profile.version = 0x0000; + profile.report_id = bus_type == BusType::bluetooth ? 0x11 : 1; + profile.input_report_size = + bus_type == BusType::bluetooth ? dualshock4_bluetooth_input_report_size : dualshock4_usb_input_report_size; + profile.output_report_size = + bus_type == BusType::bluetooth ? dualshock4_bluetooth_output_report_size : dualshock4_usb_output_report_size; + profile.name = "Wireless Controller"; + profile.manufacturer = "Sony Interactive Entertainment"; + profile.capabilities = { + .supports_rumble = true, + .supports_motion = true, + .supports_touchpad = true, + .supports_rgb_led = true, + .supports_battery = true, + }; + profile.report_descriptor = + bus_type == BusType::bluetooth ? make_dualshock4_bluetooth_report_descriptor() : make_dualshock4_usb_report_descriptor(); + return profile; + } + DeviceProfile make_dualsense_profile(BusType bus_type) { DeviceProfile profile; profile.device_type = DeviceType::gamepad; @@ -828,6 +1736,18 @@ namespace lvh::profiles { ); } + DeviceProfile dualshock4() { + return dualshock4_usb(); + } + + DeviceProfile dualshock4_usb() { + return make_dualshock4_profile(BusType::usb); + } + + DeviceProfile dualshock4_bluetooth() { + return make_dualshock4_profile(BusType::bluetooth); + } + DeviceProfile dualsense() { return dualsense_usb(); } @@ -883,6 +1803,8 @@ namespace lvh::profiles { return xbox_one(); case GamepadProfileKind::xbox_series: return xbox_series(); + case GamepadProfileKind::dualshock4: + return dualshock4(); case GamepadProfileKind::dualsense: return dualsense(); case GamepadProfileKind::switch_pro: @@ -898,6 +1820,7 @@ namespace lvh::profiles { xbox_360(), xbox_one(), xbox_series(), + dualshock4(), dualsense(), switch_pro(), }; diff --git a/src/core/report.cpp b/src/core/report.cpp index d90027f..d0406f6 100644 --- a/src/core/report.cpp +++ b/src/core/report.cpp @@ -6,6 +6,7 @@ // standard includes #include #include +#include #include #include #include @@ -26,6 +27,18 @@ namespace lvh::reports { constexpr auto zero_byte = std::byte {0x00}; + constexpr auto dualshock4_usb_output_report_id = std::byte {0x05}; + + constexpr auto dualshock4_bt_input_report_id = std::byte {0x11}; + + constexpr auto dualshock4_bt_output_report_id = std::byte {0x11}; + + constexpr auto dualshock4_output_hwctl_crc32 = std::byte {0x40}; + + constexpr auto dualshock4_flag0_rumble = std::byte {0x01}; + + constexpr auto dualshock4_flag0_lightbar = std::byte {0x02}; + constexpr auto dualsense_usb_output_report_id = std::byte {0x02}; constexpr auto dualsense_bt_input_report_id = std::byte {0x31}; @@ -34,9 +47,9 @@ namespace lvh::reports { constexpr auto dualsense_bt_input_report_reserved = std::byte {0x00}; - constexpr auto dualsense_input_crc_seed = std::byte {0xA1}; + constexpr auto playstation_input_crc_seed = std::byte {0xA1}; - constexpr auto dualsense_output_crc_seed = std::byte {0xA2}; + constexpr auto playstation_output_crc_seed = std::byte {0xA2}; constexpr auto dualsense_flag0_rumble = std::byte {0x01}; @@ -136,18 +149,18 @@ namespace lvh::reports { return crc ^ 0xFFFFFFFFU; } - std::uint32_t dualsense_crc_seed(std::byte seed) { + std::uint32_t playstation_crc_seed(std::byte seed) { const std::array seed_report {seed}; return crc32(seed_report); } - void write_dualsense_crc(ByteReport &report, std::byte seed) { + void write_playstation_crc(ByteReport &report, std::byte seed) { if (report.size() < 4U) { return; } const auto crc_offset = report.size() - 4U; - write_u32(report, crc_offset, crc32({report.data(), crc_offset}, dualsense_crc_seed(seed))); + write_u32(report, crc_offset, crc32({report.data(), crc_offset}, playstation_crc_seed(seed))); } std::int16_t scale_i16(float value, float multiplier) { @@ -155,7 +168,7 @@ namespace lvh::reports { return static_cast(std::lround(scaled)); } - std::uint8_t normalize_dualsense_axis(float value) { + std::uint8_t normalize_u8_axis(float value) { return static_cast(std::lround((clamp_axis(value) + 1.0F) * 127.5F)); } @@ -224,6 +237,145 @@ namespace lvh::reports { report[offset + 3U] = to_low_byte(y >> 4U); } + void write_dualshock4_touch_contact( + ByteReport &report, + std::size_t offset, + const GamepadTouchContact &contact + ) { + const auto x = static_cast(std::lround(std::clamp(contact.x, 0.0F, 1.0F) * 1919.0F)); + const auto y = static_cast(std::lround(std::clamp(contact.y, 0.0F, 1.0F) * 941.0F)); + report[offset] = (to_byte(contact.id) & std::byte {0x7F}) | (contact.active ? zero_byte : std::byte {0x80}); + report[offset + 1U] = to_low_byte(x); + report[offset + 2U] = to_low_byte(((x >> 8U) & 0x0FU) | ((y & 0x0FU) << 4U)); + report[offset + 3U] = to_low_byte(y >> 4U); + } + + std::uint8_t dualshock4_battery_status(const GamepadBattery &battery) { + using enum GamepadBatteryState; + + if (battery.state == full) { + return 0x1B; + } + if ( + battery.state == voltage_or_temperature_error || + battery.state == temperature_error || + battery.state == charging_error + ) { + return 0x0F; + } + + const auto charge = std::min(10U, static_cast(std::lround(battery.percentage / 10.0F))); + if (battery.state == discharging) { + return charge; + } + + return static_cast(0x10U | charge); + } + + std::uint8_t dualshock4_battery_level(const GamepadBattery &battery) { + if (battery.state == GamepadBatteryState::full && battery.percentage >= 100U) { + return 0xFF; + } + + return static_cast(std::lround((static_cast(battery.percentage) / 100.0F) * 255.0F)); + } + + std::uint16_t dualshock4_sensor_timestamp() { + static const auto start = std::chrono::steady_clock::now(); + const auto elapsed = + std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count(); + return static_cast((static_cast(elapsed) * 3U) / 16U); + } + + std::vector pack_dualshock4_input_report(const DeviceProfile &profile, const GamepadState &state) { + const auto is_bluetooth = profile.bus_type == BusType::bluetooth; + const auto payload_offset = is_bluetooth ? 3U : 1U; + if (const auto minimum_report_size = is_bluetooth ? 78U : 64U; profile.input_report_size < minimum_report_size) { + return {}; + } + + const auto normalized = normalize_state(state); + const auto acceleration = normalized.acceleration.value_or(Vector3 {.x = 0.0F, .y = 9.80665F, .z = 0.0F}); + const auto gyroscope = normalized.gyroscope.value_or(Vector3 {}); + const auto battery = normalized.battery.value_or(GamepadBattery {.state = GamepadBatteryState::full, .percentage = 100}); + + ByteReport report(profile.input_report_size, zero_byte); + report[0] = is_bluetooth ? dualshock4_bt_input_report_id : to_byte(profile.report_id); + + report[payload_offset + 0U] = to_byte(normalize_u8_axis(normalized.left_stick.x)); + report[payload_offset + 1U] = to_byte(normalize_u8_axis(normalized.left_stick.y)); + report[payload_offset + 2U] = to_byte(normalize_u8_axis(normalized.right_stick.x)); + report[payload_offset + 3U] = to_byte(normalize_u8_axis(normalized.right_stick.y)); + report[payload_offset + 4U] = to_byte(hat_from_buttons(normalized.buttons)); + + if (normalized.buttons.test(GamepadButton::x)) { + add_flag(report, payload_offset + 4U, std::byte {0x10}); + } + if (normalized.buttons.test(GamepadButton::a)) { + add_flag(report, payload_offset + 4U, std::byte {0x20}); + } + if (normalized.buttons.test(GamepadButton::b)) { + add_flag(report, payload_offset + 4U, std::byte {0x40}); + } + if (normalized.buttons.test(GamepadButton::y)) { + add_flag(report, payload_offset + 4U, std::byte {0x80}); + } + + if (normalized.buttons.test(GamepadButton::left_shoulder)) { + add_flag(report, payload_offset + 5U, std::byte {0x01}); + } + if (normalized.buttons.test(GamepadButton::right_shoulder)) { + add_flag(report, payload_offset + 5U, std::byte {0x02}); + } + if (normalized.left_trigger > 0.0F) { + add_flag(report, payload_offset + 5U, std::byte {0x04}); + } + if (normalized.right_trigger > 0.0F) { + add_flag(report, payload_offset + 5U, std::byte {0x08}); + } + if (normalized.buttons.test(GamepadButton::back)) { + add_flag(report, payload_offset + 5U, std::byte {0x10}); + } + if (normalized.buttons.test(GamepadButton::start)) { + add_flag(report, payload_offset + 5U, std::byte {0x20}); + } + if (normalized.buttons.test(GamepadButton::left_stick)) { + add_flag(report, payload_offset + 5U, std::byte {0x40}); + } + if (normalized.buttons.test(GamepadButton::right_stick)) { + add_flag(report, payload_offset + 5U, std::byte {0x80}); + } + + if (normalized.buttons.test(GamepadButton::guide)) { + add_flag(report, payload_offset + 6U, std::byte {0x01}); + } + if (normalized.buttons.test(GamepadButton::touchpad)) { + add_flag(report, payload_offset + 6U, std::byte {0x02}); + } + + report[payload_offset + 7U] = to_byte(normalize_trigger(normalized.left_trigger)); + report[payload_offset + 8U] = to_byte(normalize_trigger(normalized.right_trigger)); + write_u16(report, payload_offset + 9U, dualshock4_sensor_timestamp()); + report[payload_offset + 11U] = to_byte(dualshock4_battery_level(battery)); + write_i16(report, payload_offset + 12U, scale_i16(gyroscope.x, 20.0F)); + write_i16(report, payload_offset + 14U, scale_i16(gyroscope.y, 20.0F)); + write_i16(report, payload_offset + 16U, scale_i16(gyroscope.z, 20.0F)); + write_i16(report, payload_offset + 18U, scale_i16(acceleration.x, 10000.0F / 9.80665F)); + write_i16(report, payload_offset + 20U, scale_i16(acceleration.y, 10000.0F / 9.80665F)); + write_i16(report, payload_offset + 22U, scale_i16(acceleration.z, 10000.0F / 9.80665F)); + report[payload_offset + 29U] = to_byte(dualshock4_battery_status(battery)); + + const auto touch_report_offset = payload_offset + 33U; + report[payload_offset + 32U] = std::byte {0x01}; + write_dualshock4_touch_contact(report, touch_report_offset + 1U, normalized.touchpad_contacts[0]); + write_dualshock4_touch_contact(report, touch_report_offset + 5U, normalized.touchpad_contacts[1]); + + if (is_bluetooth) { + write_playstation_crc(report, playstation_input_crc_seed); + } + return to_uint8_report(report); + } + std::vector pack_dualsense_input_report(const DeviceProfile &profile, const GamepadState &state) { const auto is_bluetooth = profile.bus_type == BusType::bluetooth; const auto payload_offset = is_bluetooth ? 2U : 1U; @@ -238,10 +390,10 @@ namespace lvh::reports { report[1] = dualsense_bt_input_report_reserved; } - report[payload_offset + 0U] = to_byte(normalize_dualsense_axis(normalized.left_stick.x)); - report[payload_offset + 1U] = to_byte(normalize_dualsense_axis(normalized.left_stick.y)); - report[payload_offset + 2U] = to_byte(normalize_dualsense_axis(normalized.right_stick.x)); - report[payload_offset + 3U] = to_byte(normalize_dualsense_axis(normalized.right_stick.y)); + report[payload_offset + 0U] = to_byte(normalize_u8_axis(normalized.left_stick.x)); + report[payload_offset + 1U] = to_byte(normalize_u8_axis(normalized.left_stick.y)); + report[payload_offset + 2U] = to_byte(normalize_u8_axis(normalized.right_stick.x)); + report[payload_offset + 3U] = to_byte(normalize_u8_axis(normalized.right_stick.y)); report[payload_offset + 4U] = to_byte(normalize_trigger(normalized.left_trigger)); report[payload_offset + 5U] = to_byte(normalize_trigger(normalized.right_trigger)); report[payload_offset + 7U] = to_byte(hat_from_buttons(normalized.buttons)); @@ -287,6 +439,9 @@ namespace lvh::reports { if (normalized.buttons.test(GamepadButton::guide)) { add_flag(report, payload_offset + 9U, std::byte {0x01}); } + if (normalized.buttons.test(GamepadButton::touchpad)) { + add_flag(report, payload_offset + 9U, std::byte {0x02}); + } if (normalized.buttons.test(GamepadButton::misc1)) { add_flag(report, payload_offset + 9U, std::byte {0x04}); } @@ -312,7 +467,7 @@ namespace lvh::reports { report[payload_offset + 53U] = std::byte {0x0C}; if (is_bluetooth) { - write_dualsense_crc(report, dualsense_input_crc_seed); + write_playstation_crc(report, playstation_input_crc_seed); } return to_uint8_report(report); } @@ -323,7 +478,7 @@ namespace lvh::reports { } if (report.size() >= 49U && report[0] == dualsense_bt_output_report_id) { if (report.size() >= 78U) { - const auto expected_crc = crc32({report.data(), report.size() - 4U}, dualsense_crc_seed(dualsense_output_crc_seed)); + const auto expected_crc = crc32({report.data(), report.size() - 4U}, playstation_crc_seed(playstation_output_crc_seed)); const auto actual_crc = read_u32(report, report.size() - 4U); if (actual_crc != expected_crc) { return std::nullopt; @@ -339,6 +494,59 @@ namespace lvh::reports { return std::nullopt; } + std::optional dualshock4_common_output_offset(const ByteReport &report) { + if (report.size() >= 32U && report[0] == dualshock4_usb_output_report_id) { + return 1U; + } + if (report.size() >= 78U && report[0] == dualshock4_bt_output_report_id) { + if (has_flag(report[1], dualshock4_output_hwctl_crc32)) { + const auto expected_crc = crc32({report.data(), report.size() - 4U}, playstation_crc_seed(playstation_output_crc_seed)); + const auto actual_crc = read_u32(report, report.size() - 4U); + if (actual_crc != expected_crc) { + return std::nullopt; + } + } + return 3U; + } + return std::nullopt; + } + + void append_dualshock4_outputs( + const ByteReport &report, + const std::vector &raw_report, + std::size_t offset, + std::vector &outputs + ) { + const auto valid_flag0 = report[offset]; + const auto valid_flag1 = report[offset + 1U]; + const auto motor_right = raw_report[offset + 3U]; + const auto motor_left = raw_report[offset + 4U]; + + if (has_flag(valid_flag0, dualshock4_flag0_rumble)) { + GamepadOutput output; + output.kind = GamepadOutputKind::rumble; + output.low_frequency_rumble = scale_output_byte(motor_left); + output.high_frequency_rumble = scale_output_byte(motor_right); + output.raw_report = raw_report; + outputs.push_back(std::move(output)); + } else if (valid_flag0 == zero_byte && valid_flag1 == zero_byte) { + GamepadOutput output; + output.kind = GamepadOutputKind::rumble; + output.raw_report = raw_report; + outputs.push_back(std::move(output)); + } + + if (has_flag(valid_flag0, dualshock4_flag0_lightbar)) { + GamepadOutput output; + output.kind = GamepadOutputKind::rgb_led; + output.red = raw_report[offset + 5U]; + output.green = raw_report[offset + 6U]; + output.blue = raw_report[offset + 7U]; + output.raw_report = raw_report; + outputs.push_back(std::move(output)); + } + } + void append_dualsense_outputs( const ByteReport &report, const std::vector &raw_report, @@ -489,8 +697,13 @@ namespace lvh::reports { } std::vector pack_input_report(const DeviceProfile &profile, const GamepadState &state) { - if (profile.device_type == DeviceType::gamepad && profile.gamepad_kind == GamepadProfileKind::dualsense) { - return pack_dualsense_input_report(profile, state); + if (profile.device_type == DeviceType::gamepad) { + if (profile.gamepad_kind == GamepadProfileKind::dualshock4) { + return pack_dualshock4_input_report(profile, state); + } + if (profile.gamepad_kind == GamepadProfileKind::dualsense) { + return pack_dualsense_input_report(profile, state); + } } constexpr std::size_t common_report_size = 14; @@ -529,6 +742,16 @@ namespace lvh::reports { std::vector parse_output_reports(const DeviceProfile &profile, const std::vector &report) { std::vector outputs; + if (profile.gamepad_kind == GamepadProfileKind::dualshock4) { + const auto byte_report = to_byte_report(report); + if (const auto offset = dualshock4_common_output_offset(byte_report); offset.has_value()) { + append_dualshock4_outputs(byte_report, report, *offset, outputs); + } + if (!outputs.empty()) { + return outputs; + } + } + if (profile.gamepad_kind == GamepadProfileKind::dualsense) { const auto byte_report = to_byte_report(report); if (const auto offset = dualsense_common_output_offset(byte_report); offset.has_value()) { diff --git a/src/include/libvirtualhid/profiles.hpp b/src/include/libvirtualhid/profiles.hpp index 39001b7..f379d9a 100644 --- a/src/include/libvirtualhid/profiles.hpp +++ b/src/include/libvirtualhid/profiles.hpp @@ -41,6 +41,27 @@ namespace lvh::profiles { */ DeviceProfile xbox_series(); + /** + * @brief Create the PlayStation DualShock 4-compatible gamepad profile. + * + * @return Default DualShock 4-compatible device profile. + */ + DeviceProfile dualshock4(); + + /** + * @brief Create the USB PlayStation DualShock 4-compatible gamepad profile. + * + * @return USB DualShock 4-compatible device profile. + */ + DeviceProfile dualshock4_usb(); + + /** + * @brief Create the Bluetooth PlayStation DualShock 4-compatible gamepad profile. + * + * @return Bluetooth DualShock 4-compatible device profile. + */ + DeviceProfile dualshock4_bluetooth(); + /** * @brief Create the PlayStation DualSense-compatible gamepad profile. * diff --git a/src/include/libvirtualhid/types.hpp b/src/include/libvirtualhid/types.hpp index 4b15969..b065bde 100644 --- a/src/include/libvirtualhid/types.hpp +++ b/src/include/libvirtualhid/types.hpp @@ -230,6 +230,7 @@ namespace lvh { xbox_series, ///< Xbox Series-compatible profile. dualsense, ///< PlayStation DualSense-compatible profile. switch_pro, ///< Nintendo Switch Pro-compatible profile. + dualshock4, ///< PlayStation DualShock 4-compatible profile. }; /** @@ -507,6 +508,7 @@ namespace lvh { dpad_left, ///< Directional pad left. dpad_right, ///< Directional pad right. misc1, ///< Profile-specific miscellaneous button. + touchpad, ///< Touchpad click button. }; /** diff --git a/src/platform/linux/uhid_backend.cpp b/src/platform/linux/uhid_backend.cpp index c22a375..ebb0687 100644 --- a/src/platform/linux/uhid_backend.cpp +++ b/src/platform/linux/uhid_backend.cpp @@ -77,11 +77,170 @@ namespace lvh::detail { constexpr auto tablet_distance_max = 1024; constexpr auto tablet_resolution = 28; constexpr auto poll_timeout_ms = 100; + constexpr auto dualshock4_usb_calibration_report = 0x02; + constexpr auto dualshock4_bluetooth_calibration_report = 0x05; + constexpr auto dualshock4_pairing_report = 0x12; + constexpr auto dualshock4_firmware_report = 0xA3; + constexpr auto playstation_periodic_report_ms = 10; + constexpr std::uint8_t playstation_feature_crc_seed = 0xA3; constexpr auto dualsense_calibration_report = 0x05; constexpr auto dualsense_pairing_report = 0x09; constexpr auto dualsense_firmware_report = 0x20; - constexpr auto dualsense_periodic_report_ms = 10; - constexpr std::uint8_t dualsense_feature_crc_seed = 0xA3; + + constexpr std::uint8_t dualshock4_usb_calibration_info[] { + 0x02, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x10, + 0x27, + 0xF0, + 0xD8, + 0x10, + 0x27, + 0xF0, + 0xD8, + 0x10, + 0x27, + 0xF0, + 0xD8, + 0xF4, + 0x01, + 0xF4, + 0x01, + 0x10, + 0x27, + 0xF0, + 0xD8, + 0x10, + 0x27, + 0xF0, + 0xD8, + 0x10, + 0x27, + 0xF0, + 0xD8, + 0x00, + 0x00, + }; + + constexpr std::uint8_t dualshock4_bluetooth_calibration_info[] { + 0x05, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x10, + 0x27, + 0xF0, + 0xD8, + 0x10, + 0x27, + 0xF0, + 0xD8, + 0x10, + 0x27, + 0xF0, + 0xD8, + 0xF4, + 0x01, + 0xF4, + 0x01, + 0x10, + 0x27, + 0xF0, + 0xD8, + 0x10, + 0x27, + 0xF0, + 0xD8, + 0x10, + 0x27, + 0xF0, + 0xD8, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + }; + + constexpr std::uint8_t dualshock4_firmware_info[] { + 0xA3, + 0x41, + 0x75, + 0x67, + 0x20, + 0x20, + 0x33, + 0x20, + 0x32, + 0x30, + 0x31, + 0x33, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x30, + 0x37, + 0x3A, + 0x30, + 0x31, + 0x3A, + 0x31, + 0x32, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x31, + 0x03, + 0x00, + 0x00, + 0x00, + 0x49, + 0x00, + 0x05, + 0x00, + 0x00, + 0x80, + 0x03, + 0x00, + }; + + constexpr std::uint8_t dualshock4_pairing_info[] { + 0x12, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + }; constexpr std::uint8_t dualsense_calibration_info[] { 0x05, @@ -316,7 +475,7 @@ namespace lvh::detail { return crc ^ 0xFFFFFFFFU; } - std::uint32_t dualsense_crc_seed(std::uint8_t seed) { + std::uint32_t playstation_crc_seed(std::uint8_t seed) { return crc32(std::span {&seed, 1U}); } @@ -327,6 +486,10 @@ namespace lvh::detail { buffer[3] = static_cast((value >> 24U) & 0xFFU); } + bool is_playstation_profile(GamepadProfileKind kind) { + return kind == GamepadProfileKind::dualshock4 || kind == GamepadProfileKind::dualsense; + } + std::uint16_t to_uhid_bus(BusType bus_type) { if (bus_type == BusType::bluetooth) { return BUS_BLUETOOTH; @@ -2096,9 +2259,9 @@ namespace lvh::detail { event.type = UHID_CREATE2; auto unique_id = options.metadata.stable_id.empty() ? std::to_string(id) : options.metadata.stable_id; - if (options.profile.gamepad_kind == GamepadProfileKind::dualsense) { - dualsense_mac_address_ = parse_mac_address(options.metadata.stable_id).value_or(generated_mac_address(id)); - unique_id = format_mac_address(dualsense_mac_address_); + if (is_playstation_profile(options.profile.gamepad_kind)) { + playstation_mac_address_ = parse_mac_address(options.metadata.stable_id).value_or(generated_mac_address(id)); + unique_id = format_mac_address(playstation_mac_address_); } copy_string(request.name, options.profile.name); @@ -2125,7 +2288,7 @@ namespace lvh::detail { reader_ = std::jthread {[this](std::stop_token stop_token) { read_loop(stop_token); }}; - if (profile_.gamepad_kind == GamepadProfileKind::dualsense) { + if (is_playstation_profile(profile_.gamepad_kind)) { periodic_reporter_ = std::jthread {[this](std::stop_token stop_token) { periodic_report_loop(stop_token); }}; @@ -2279,7 +2442,7 @@ namespace lvh::detail { void periodic_report_loop(std::stop_token stop_token) { while (!stop_token.stop_requested() && running_) { - std::this_thread::sleep_for(std::chrono::milliseconds {dualsense_periodic_report_ms}); + std::this_thread::sleep_for(std::chrono::milliseconds {playstation_periodic_report_ms}); if (stop_token.stop_requested() || !running_ || !open_) { break; } @@ -2319,7 +2482,34 @@ namespace lvh::detail { event.u.get_report_reply.id = id; event.u.get_report_reply.err = EIO; - if (profile_.gamepad_kind == GamepadProfileKind::dualsense) { + if (profile_.gamepad_kind == GamepadProfileKind::dualshock4) { + event.u.get_report_reply.err = 0; + switch (report_number) { + case dualshock4_usb_calibration_report: + copy_get_report_payload(event, dualshock4_usb_calibration_info); + break; + case dualshock4_bluetooth_calibration_report: + if (profile_.bus_type == BusType::bluetooth) { + copy_get_report_payload(event, dualshock4_bluetooth_calibration_info); + break; + } + event.u.get_report_reply.err = EINVAL; + break; + case dualshock4_pairing_report: + copy_get_report_payload(event, dualshock4_pairing_info); + for (std::size_t index = 0; index < playstation_mac_address_.size(); ++index) { + event.u.get_report_reply.data[1U + index] = + playstation_mac_address_[playstation_mac_address_.size() - 1U - index]; + } + break; + case dualshock4_firmware_report: + copy_get_report_payload(event, dualshock4_firmware_info); + break; + default: + event.u.get_report_reply.err = EINVAL; + break; + } + } else if (profile_.gamepad_kind == GamepadProfileKind::dualsense) { event.u.get_report_reply.err = 0; switch (report_number) { case dualsense_calibration_report: @@ -2327,9 +2517,9 @@ namespace lvh::detail { break; case dualsense_pairing_report: copy_get_report_payload(event, dualsense_pairing_info); - for (std::size_t index = 0; index < dualsense_mac_address_.size(); ++index) { + for (std::size_t index = 0; index < playstation_mac_address_.size(); ++index) { event.u.get_report_reply.data[1U + index] = - dualsense_mac_address_[dualsense_mac_address_.size() - 1U - index]; + playstation_mac_address_[playstation_mac_address_.size() - 1U - index]; } break; case dualsense_firmware_report: @@ -2339,15 +2529,18 @@ namespace lvh::detail { event.u.get_report_reply.err = EINVAL; break; } + } - if (profile_.bus_type == BusType::bluetooth && event.u.get_report_reply.err == 0 && event.u.get_report_reply.size >= 4U) { - const auto crc_offset = static_cast(event.u.get_report_reply.size) - 4U; - const auto crc = crc32( - std::span {event.u.get_report_reply.data, crc_offset}, - dualsense_crc_seed(dualsense_feature_crc_seed) - ); - write_u32_le(event.u.get_report_reply.data + crc_offset, crc); - } + if ( + profile_.bus_type == BusType::bluetooth && is_playstation_profile(profile_.gamepad_kind) && + event.u.get_report_reply.err == 0 && event.u.get_report_reply.size >= 4U + ) { + const auto crc_offset = static_cast(event.u.get_report_reply.size) - 4U; + const auto crc = crc32( + std::span {event.u.get_report_reply.data, crc_offset}, + playstation_crc_seed(playstation_feature_crc_seed) + ); + write_u32_le(event.u.get_report_reply.data + crc_offset, crc); } static_cast(write_event(event)); @@ -2369,7 +2562,7 @@ namespace lvh::detail { int fd_ = -1; DeviceProfile profile_; std::string device_name_; - std::array dualsense_mac_address_ {}; + std::array playstation_mac_address_ {}; std::vector last_report_; std::atomic_bool open_ = true; std::atomic_bool running_ = false; diff --git a/src/platform/windows/control_protocol.hpp b/src/platform/windows/control_protocol.hpp index d4b9eb0..a1463dc 100644 --- a/src/platform/windows/control_protocol.hpp +++ b/src/platform/windows/control_protocol.hpp @@ -75,6 +75,8 @@ namespace lvh::detail::windows { return LVH_WINDOWS_GAMEPAD_XBOX_ONE; case xbox_series: return LVH_WINDOWS_GAMEPAD_XBOX_SERIES; + case dualshock4: + return LVH_WINDOWS_GAMEPAD_DUALSHOCK4; case dualsense: return LVH_WINDOWS_GAMEPAD_DUALSENSE; case switch_pro: diff --git a/src/platform/windows/shared/lvh_windows_protocol.h b/src/platform/windows/shared/lvh_windows_protocol.h index 74f4f55..26e466e 100644 --- a/src/platform/windows/shared/lvh_windows_protocol.h +++ b/src/platform/windows/shared/lvh_windows_protocol.h @@ -86,6 +86,7 @@ enum class LvhWindowsGamepadProfileKind : uint32_t { xbox_series = 3, dualsense = 4, switch_pro = 5, + dualshock4 = 6, }; namespace lvh_windows_protocol_detail { @@ -112,6 +113,7 @@ namespace lvh_windows_protocol_detail { inline constexpr uint32_t gamepad_xbox_series = to_uint32(xbox_series); inline constexpr uint32_t gamepad_dualsense = to_uint32(dualsense); inline constexpr uint32_t gamepad_switch_pro = to_uint32(switch_pro); + inline constexpr uint32_t gamepad_dualshock4 = to_uint32(dualshock4); } // namespace lvh_windows_protocol_detail inline constexpr uint32_t LVH_WINDOWS_STATUS_SUCCESS = lvh_windows_protocol_detail::status_success; @@ -132,6 +134,8 @@ inline constexpr uint32_t LVH_WINDOWS_GAMEPAD_XBOX_ONE = lvh_windows_protocol_de inline constexpr uint32_t LVH_WINDOWS_GAMEPAD_XBOX_SERIES = lvh_windows_protocol_detail::gamepad_xbox_series; inline constexpr uint32_t LVH_WINDOWS_GAMEPAD_DUALSENSE = lvh_windows_protocol_detail::gamepad_dualsense; inline constexpr uint32_t LVH_WINDOWS_GAMEPAD_SWITCH_PRO = lvh_windows_protocol_detail::gamepad_switch_pro; +inline constexpr uint32_t LVH_WINDOWS_GAMEPAD_DUALSHOCK4 = + lvh_windows_protocol_detail::gamepad_dualshock4; #else @@ -184,6 +188,7 @@ enum LvhWindowsGamepadProfileKind { LVH_WINDOWS_GAMEPAD_XBOX_SERIES = 3, LVH_WINDOWS_GAMEPAD_DUALSENSE = 4, LVH_WINDOWS_GAMEPAD_SWITCH_PRO = 5, + LVH_WINDOWS_GAMEPAD_DUALSHOCK4 = 6, }; #endif diff --git a/tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp b/tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp index 3a93655..3c98cab 100644 --- a/tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp +++ b/tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp @@ -90,26 +90,51 @@ namespace lvh::detail::test { */ bool saw_dualsense_calibration = false; + /** + * @brief Whether the peer observed a DualShock 4 calibration reply. + */ + bool saw_dualshock4_calibration = false; + /** * @brief Whether the peer observed a DualSense pairing reply. */ bool saw_dualsense_pairing = false; + /** + * @brief Whether the peer observed a DualShock 4 pairing reply. + */ + bool saw_dualshock4_pairing = false; + /** * @brief Whether the peer observed a DualSense firmware reply. */ bool saw_dualsense_firmware = false; + /** + * @brief Whether the peer observed a DualShock 4 firmware reply. + */ + bool saw_dualshock4_firmware = false; + /** * @brief Whether the peer observed a signed Bluetooth DualSense feature reply. */ bool saw_dualsense_feature_crc = false; + /** + * @brief Whether the peer observed a signed Bluetooth DualShock 4 feature reply. + */ + bool saw_dualshock4_feature_crc = false; + /** * @brief Whether the peer observed a Bluetooth-framed DualSense input report. */ bool saw_dualsense_bluetooth_input = false; + /** + * @brief Whether the peer observed a Bluetooth-framed DualShock 4 input report. + */ + bool saw_dualshock4_bluetooth_input = false; + /** * @brief Whether the peer observed a set-report reply. */ @@ -659,6 +684,20 @@ namespace lvh::detail::test { */ LinuxUhidRoundTripResult linux_dualsense_bluetooth_uhid_socketpair_reports(); + /** + * @brief Exercise DualShock 4 UHID feature-report replies over a socketpair. + * + * @return Round-trip result with feature-report observations. + */ + LinuxUhidRoundTripResult linux_dualshock4_uhid_socketpair_reports(); + + /** + * @brief Exercise Bluetooth DualShock 4 UHID framing and signed feature replies over a socketpair. + * + * @return Round-trip result with Bluetooth framing observations. + */ + LinuxUhidRoundTripResult linux_dualshock4_bluetooth_uhid_socketpair_reports(); + /** * @brief Create all Linux backend device types using fake successful syscalls. * diff --git a/tests/fixtures/linux_backend_test_hooks.cpp b/tests/fixtures/linux_backend_test_hooks.cpp index eba1d36..fa1c754 100644 --- a/tests/fixtures/linux_backend_test_hooks.cpp +++ b/tests/fixtures/linux_backend_test_hooks.cpp @@ -1378,7 +1378,7 @@ namespace lvh::detail::test { const auto report_size = static_cast(event.u.input2.size); if (report_size == options.profile.input_report_size && event.u.input2.data[0] == 0x31) { const auto crc_offset = report_size - 4U; - const auto expected_crc = crc32(std::span {event.u.input2.data, crc_offset}, dualsense_crc_seed(0xA1)); + const auto expected_crc = crc32(std::span {event.u.input2.data, crc_offset}, playstation_crc_seed(0xA1)); const auto actual_crc = read_u32_le(event.u.input2.data + crc_offset); result.saw_dualsense_bluetooth_input = expected_crc == actual_crc; } @@ -1399,7 +1399,7 @@ namespace lvh::detail::test { const auto crc_offset = report_size - 4U; const auto expected_crc = crc32( std::span {event.u.get_report_reply.data, crc_offset}, - dualsense_crc_seed(dualsense_feature_crc_seed) + playstation_crc_seed(playstation_feature_crc_seed) ); const auto actual_crc = read_u32_le(event.u.get_report_reply.data + crc_offset); result.saw_dualsense_feature_crc = expected_crc == actual_crc; @@ -1412,6 +1412,139 @@ namespace lvh::detail::test { return result; } + LinuxUhidRoundTripResult linux_dualshock4_uhid_socketpair_reports() { + LinuxUhidRoundTripResult result; + std::array descriptors {-1, -1}; + if (::socketpair(AF_UNIX, SOCK_STREAM, 0, descriptors.data()) != 0) { + result.create_status = system_error_status(ErrorCode::backend_failure, "failed to create socketpair", errno); + result.submit_status = result.create_status; + result.close_status = result.create_status; + return result; + } + + CreateGamepadOptions options; + options.profile = profiles::dualshock4_usb(); + options.metadata.stable_id = "02:03:04:05:06:07"; + + UhidGamepad gamepad {descriptors[0]}; + result.create_status = gamepad.create(10, options); + + uhid_event event {}; + if (read_uhid_event_type(descriptors[1], UHID_CREATE2, event)) { + result.saw_create = event.u.create2.vendor == options.profile.vendor_id && + event.u.create2.product == options.profile.product_id; + } + + event = {}; + event.type = UHID_GET_REPORT; + event.u.get_report.id = 16; + event.u.get_report.rnum = 0x02; + static_cast(write_uhid_event(descriptors[1], event)); + if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) { + result.saw_dualshock4_calibration = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size == 37 && + event.u.get_report_reply.data[0] == 0x02; + } + + event = {}; + event.type = UHID_GET_REPORT; + event.u.get_report.id = 17; + event.u.get_report.rnum = 0x12; + static_cast(write_uhid_event(descriptors[1], event)); + if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) { + result.saw_dualshock4_pairing = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size == 16 && + event.u.get_report_reply.data[0] == 0x12 && + event.u.get_report_reply.data[1] == 0x07 && + event.u.get_report_reply.data[6] == 0x02; + } + + event = {}; + event.type = UHID_GET_REPORT; + event.u.get_report.id = 18; + event.u.get_report.rnum = 0xA3; + static_cast(write_uhid_event(descriptors[1], event)); + if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) { + result.saw_dualshock4_firmware = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size == 49 && + event.u.get_report_reply.data[0] == 0xA3; + } + + result.close_status = gamepad.close(); + static_cast(::close(descriptors[1])); + result.submit_status = OperationStatus::success(); + return result; + } + + LinuxUhidRoundTripResult linux_dualshock4_bluetooth_uhid_socketpair_reports() { + LinuxUhidRoundTripResult result; + std::array descriptors {-1, -1}; + if (::socketpair(AF_UNIX, SOCK_STREAM, 0, descriptors.data()) != 0) { + result.create_status = system_error_status(ErrorCode::backend_failure, "failed to create socketpair", errno); + result.submit_status = result.create_status; + result.close_status = result.create_status; + return result; + } + + CreateGamepadOptions options; + options.profile = profiles::dualshock4_bluetooth(); + options.metadata.stable_id = "02:03:04:05:06:07"; + + UhidGamepad gamepad {descriptors[0]}; + result.create_status = gamepad.create(11, options); + + uhid_event event {}; + if (read_uhid_event_type(descriptors[1], UHID_CREATE2, event)) { + result.saw_create = event.u.create2.vendor == options.profile.vendor_id && + event.u.create2.product == options.profile.product_id && + event.u.create2.bus == BUS_BLUETOOTH; + } + + if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) { + const auto report_size = static_cast(event.u.input2.size); + if (report_size == options.profile.input_report_size && event.u.input2.data[0] == 0x11) { + const auto crc_offset = report_size - 4U; + const auto expected_crc = crc32(std::span {event.u.input2.data, crc_offset}, playstation_crc_seed(0xA1)); + const auto actual_crc = read_u32_le(event.u.input2.data + crc_offset); + result.saw_dualshock4_bluetooth_input = expected_crc == actual_crc; + } + } + + event = {}; + event.type = UHID_GET_REPORT; + event.u.get_report.id = 19; + event.u.get_report.rnum = 0x05; + static_cast(write_uhid_event(descriptors[1], event)); + if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) { + const auto report_size = static_cast(event.u.get_report_reply.size); + result.saw_dualshock4_calibration = event.u.get_report_reply.err == 0 && report_size == 41U && + event.u.get_report_reply.data[0] == 0x05; + if (report_size >= 4U) { + const auto crc_offset = report_size - 4U; + const auto expected_crc = crc32( + std::span {event.u.get_report_reply.data, crc_offset}, + playstation_crc_seed(playstation_feature_crc_seed) + ); + const auto actual_crc = read_u32_le(event.u.get_report_reply.data + crc_offset); + result.saw_dualshock4_feature_crc = expected_crc == actual_crc; + } + } + + event = {}; + event.type = UHID_GET_REPORT; + event.u.get_report.id = 20; + event.u.get_report.rnum = 0x12; + static_cast(write_uhid_event(descriptors[1], event)); + if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) { + result.saw_dualshock4_pairing = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size == 16 && + event.u.get_report_reply.data[0] == 0x12 && + event.u.get_report_reply.data[1] == 0x07 && + event.u.get_report_reply.data[6] == 0x02; + } + + result.close_status = gamepad.close(); + static_cast(::close(descriptors[1])); + result.submit_status = OperationStatus::success(); + return result; + } + LinuxBackendFakeCreationResult linux_backend_create_all_fake_success() { LinuxTestSyscalls syscalls; enable_fake_device_syscalls(syscalls); diff --git a/tests/unit/test_linux_backend.cpp b/tests/unit/test_linux_backend.cpp index 0ab9778..a5b2c92 100644 --- a/tests/unit/test_linux_backend.cpp +++ b/tests/unit/test_linux_backend.cpp @@ -567,6 +567,27 @@ TEST_F(LinuxBackendTest, SocketpairBackedDualSenseBluetoothFramesReports) { EXPECT_TRUE(result.saw_dualsense_feature_crc); } +TEST_F(LinuxBackendTest, SocketpairBackedDualShock4RepliesToFeatureReports) { + const auto result = lvh::detail::test::linux_dualshock4_uhid_socketpair_reports(); + EXPECT_TRUE(result.create_status.ok()) << result.create_status.message(); + EXPECT_TRUE(result.close_status.ok()) << result.close_status.message(); + EXPECT_TRUE(result.saw_create); + EXPECT_TRUE(result.saw_dualshock4_calibration); + EXPECT_TRUE(result.saw_dualshock4_pairing); + EXPECT_TRUE(result.saw_dualshock4_firmware); +} + +TEST_F(LinuxBackendTest, SocketpairBackedDualShock4BluetoothFramesReports) { + const auto result = lvh::detail::test::linux_dualshock4_bluetooth_uhid_socketpair_reports(); + EXPECT_TRUE(result.create_status.ok()) << result.create_status.message(); + EXPECT_TRUE(result.close_status.ok()) << result.close_status.message(); + EXPECT_TRUE(result.saw_create); + EXPECT_TRUE(result.saw_dualshock4_bluetooth_input); + EXPECT_TRUE(result.saw_dualshock4_calibration); + EXPECT_TRUE(result.saw_dualshock4_pairing); + EXPECT_TRUE(result.saw_dualshock4_feature_crc); +} + TEST_F(LinuxBackendTest, FakeLinuxBackendCreatesAllDeviceTypes) { const auto unavailable = lvh::detail::test::linux_backend_fake_unavailable_capabilities(); EXPECT_FALSE(unavailable.supports_virtual_hid); diff --git a/tests/unit/test_linux_consumers.cpp b/tests/unit/test_linux_consumers.cpp index d15e8fa..7d684a4 100644 --- a/tests/unit/test_linux_consumers.cpp +++ b/tests/unit/test_linux_consumers.cpp @@ -326,6 +326,7 @@ namespace { void configure_sdl_hidapi_hints() { SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); SDL_SetHint("SDL_JOYSTICK_HIDAPI", "1"); + SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS4", "1"); SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS5", "1"); } @@ -373,7 +374,7 @@ namespace { EXPECT_GE(SDL_JoystickNumAxes(joystick), minimum_axes); } - void expect_sdl_dualsense_controller_profile(SDL_GameController *controller) { + void expect_sdl_playstation_controller_profile(SDL_GameController *controller) { auto *mapping = SDL_GameControllerMapping(controller); EXPECT_NE(mapping, nullptr) << SDL_GetError(); if (mapping != nullptr) { @@ -381,7 +382,7 @@ namespace { } } - void exercise_sdl_dualsense_controller( + void exercise_sdl_playstation_controller( const SdlGamepadConsumerCase &test_case, const lvh::DeviceProfile &expected_profile, int joystick_index, @@ -416,7 +417,7 @@ namespace { state.touchpad_contacts[0] = {.id = 1, .active = true, .x = 0.5F, .y = 0.25F}; ASSERT_TRUE(gamepad.submit(state).ok()); - expect_sdl_dualsense_controller_profile(controller.get()); + expect_sdl_playstation_controller_profile(controller.get()); if (test_case.expect_live_input) { EXPECT_TRUE(wait_for_sdl_controller_input(controller.get())) << describe_sdl_controller_state(controller.get()); } @@ -446,12 +447,12 @@ namespace { ); } - void run_sdl_dualsense_controller_test(const SdlGamepadConsumerCase &test_case) { + void run_sdl_playstation_controller_test(const SdlGamepadConsumerCase &test_case) { run_sdl_gamepad_test( test_case, SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_EVENTS, [&test_case](const auto &expected_profile, int joystick_index, lvh::Gamepad &gamepad) { - exercise_sdl_dualsense_controller(test_case, expected_profile, joystick_index, gamepad); + exercise_sdl_playstation_controller(test_case, expected_profile, joystick_index, gamepad); } ); } @@ -543,7 +544,7 @@ TEST_F(LinuxConsumerTest, SdlSeesUhidGamepadButtonAndAxisInput) { TEST_F(LinuxConsumerTest, SdlSeesDualSenseUsbControllerBehavior) { ASSERT_TRUE(HasReadableWritableDeviceNode("/dev/uhid")); - run_sdl_dualsense_controller_test({ + run_sdl_playstation_controller_test({ .profile = lvh::profiles::dualsense_usb(), .name_suffix = "SDL DualSense USB", .stable_id = "02:00:00:00:00:01", @@ -552,10 +553,35 @@ TEST_F(LinuxConsumerTest, SdlSeesDualSenseUsbControllerBehavior) { }); } +TEST_F(LinuxConsumerTest, SdlSeesDualShock4UsbControllerBehavior) { + ASSERT_TRUE(HasReadableWritableDeviceNode("/dev/uhid")); + + run_sdl_playstation_controller_test({ + .profile = lvh::profiles::dualshock4_usb(), + .name_suffix = "SDL DualShock 4 USB", + .stable_id = "02:00:00:00:00:03", + .minimum_buttons = 10, + .minimum_axes = 4, + }); +} + +TEST_F(LinuxConsumerTest, SdlSeesDualShock4BluetoothControllerDiscovery) { + ASSERT_TRUE(HasReadableWritableDeviceNode("/dev/uhid")); + + run_sdl_playstation_controller_test({ + .profile = lvh::profiles::dualshock4_bluetooth(), + .name_suffix = "SDL DualShock 4 Bluetooth", + .stable_id = "02:00:00:00:00:04", + .minimum_buttons = 10, + .minimum_axes = 4, + .expect_live_input = false, + }); +} + TEST_F(LinuxConsumerTest, SdlSeesDualSenseBluetoothControllerDiscovery) { ASSERT_TRUE(HasReadableWritableDeviceNode("/dev/uhid")); - run_sdl_dualsense_controller_test({ + run_sdl_playstation_controller_test({ .profile = lvh::profiles::dualsense_bluetooth(), .name_suffix = "SDL DualSense Bluetooth", .stable_id = "02:00:00:00:00:02", @@ -795,6 +821,10 @@ TEST_F(LinuxConsumerTest, SdlSeesUhidGamepadButtonAndAxisInput) {} TEST_F(LinuxConsumerTest, SdlSeesDualSenseUsbControllerBehavior) {} +TEST_F(LinuxConsumerTest, SdlSeesDualShock4UsbControllerBehavior) {} + +TEST_F(LinuxConsumerTest, SdlSeesDualShock4BluetoothControllerDiscovery) {} + TEST_F(LinuxConsumerTest, SdlSeesDualSenseBluetoothControllerDiscovery) {} TEST_F(LinuxConsumerTest, LibinputSeesUinputKeyboardKeys) {} diff --git a/tests/unit/test_profiles.cpp b/tests/unit/test_profiles.cpp index 62b97d6..8a35d52 100644 --- a/tests/unit/test_profiles.cpp +++ b/tests/unit/test_profiles.cpp @@ -25,6 +25,7 @@ TEST(ProfileTest, BuiltInProfilesHaveDescriptors) { TEST(ProfileTest, StreamingControllerProfilesArePresent) { const auto xbox_one = lvh::profiles::xbox_one(); + const auto dualshock4 = lvh::profiles::dualshock4(); const auto dualsense = lvh::profiles::dualsense(); const auto switch_pro = lvh::profiles::switch_pro(); @@ -32,6 +33,24 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { EXPECT_EQ(xbox_one.product_id, 0x02EA); EXPECT_TRUE(xbox_one.capabilities.supports_rumble); + EXPECT_EQ(dualshock4.vendor_id, 0x054C); + EXPECT_EQ(dualshock4.product_id, 0x05C4); + EXPECT_EQ(dualshock4.input_report_size, 64U); + EXPECT_EQ(dualshock4.output_report_size, 32U); + EXPECT_TRUE(dualshock4.capabilities.supports_motion); + EXPECT_TRUE(dualshock4.capabilities.supports_touchpad); + EXPECT_TRUE(dualshock4.capabilities.supports_rgb_led); + EXPECT_TRUE(dualshock4.capabilities.supports_battery); + EXPECT_FALSE(dualshock4.capabilities.supports_adaptive_triggers); + EXPECT_EQ(dualshock4.manufacturer, "Sony Interactive Entertainment"); + + const auto dualshock4_bluetooth = lvh::profiles::dualshock4_bluetooth(); + EXPECT_EQ(dualshock4_bluetooth.bus_type, lvh::BusType::bluetooth); + EXPECT_EQ(dualshock4_bluetooth.report_id, 0x11); + EXPECT_EQ(dualshock4_bluetooth.input_report_size, 78U); + EXPECT_EQ(dualshock4_bluetooth.output_report_size, 78U); + EXPECT_NE(dualshock4_bluetooth.report_descriptor, dualshock4.report_descriptor); + EXPECT_EQ(dualsense.vendor_id, 0x054C); EXPECT_TRUE(dualsense.capabilities.supports_motion); EXPECT_TRUE(dualsense.capabilities.supports_touchpad); diff --git a/tests/unit/test_report.cpp b/tests/unit/test_report.cpp index ebe58ca..671126d 100644 --- a/tests/unit/test_report.cpp +++ b/tests/unit/test_report.cpp @@ -27,7 +27,7 @@ namespace { return crc ^ 0xFFFFFFFFU; } - std::uint32_t test_dualsense_crc_seed(std::uint8_t seed) { + std::uint32_t test_playstation_crc_seed(std::uint8_t seed) { return test_crc32(std::span {&seed, 1U}); } @@ -147,7 +147,79 @@ TEST(ReportTest, PacksDualSenseBluetoothReportWithCrc) { EXPECT_EQ(report[55], 0x0C); const auto crc_offset = report.size() - 4U; - const auto expected_crc = test_crc32(std::span {report}.first(crc_offset), test_dualsense_crc_seed(0xA1)); + const auto expected_crc = test_crc32(std::span {report}.first(crc_offset), test_playstation_crc_seed(0xA1)); + EXPECT_EQ(read_u32_le(report, crc_offset), expected_crc); +} + +TEST(ReportTest, PacksDualShock4UsbReport) { + using enum lvh::GamepadButton; + + const auto profile = lvh::profiles::dualshock4_usb(); + + lvh::GamepadState state; + state.buttons.set(x); + state.buttons.set(a); + state.buttons.set(b); + state.buttons.set(y); + state.buttons.set(left_shoulder); + state.buttons.set(right_shoulder); + state.buttons.set(back); + state.buttons.set(start); + state.buttons.set(left_stick); + state.buttons.set(right_stick); + state.buttons.set(guide); + state.buttons.set(touchpad); + state.left_stick = {1.0F, -1.0F}; + state.right_stick = {0.0F, 0.0F}; + state.left_trigger = 1.0F; + state.right_trigger = 0.5F; + state.acceleration = lvh::Vector3 {.x = 0.0F, .y = 9.80665F, .z = 0.0F}; + state.gyroscope = lvh::Vector3 {.x = 1.0F, .y = 2.0F, .z = 3.0F}; + state.battery = lvh::GamepadBattery {.state = lvh::GamepadBatteryState::charging, .percentage = 80}; + state.touchpad_contacts[0] = {.id = 3, .active = true, .x = 0.5F, .y = 0.25F}; + + const auto report = lvh::reports::pack_input_report(profile, state); + + ASSERT_EQ(report.size(), profile.input_report_size); + EXPECT_EQ(report[0], 0x01); + EXPECT_EQ(report[1], 255); + EXPECT_EQ(report[2], 0); + EXPECT_EQ(report[3], 128); + EXPECT_EQ(report[4], 128); + EXPECT_EQ(report[5], 0xF8); + EXPECT_EQ(report[6], 0xFF); + EXPECT_EQ(report[7], 0x03); + EXPECT_EQ(report[8], 255); + EXPECT_EQ(report[9], 128); + EXPECT_EQ(report[12], 204); + EXPECT_EQ(report[13], 20); + EXPECT_EQ(report[21], 0x10); + EXPECT_EQ(report[22], 0x27); + EXPECT_EQ(report[30], 0x18); + EXPECT_EQ(report[33], 1); + EXPECT_EQ(report[35] & 0x7F, 3); + EXPECT_EQ(report[35] & 0x80, 0); +} + +TEST(ReportTest, PacksDualShock4BluetoothReportWithCrc) { + const auto profile = lvh::profiles::dualshock4_bluetooth(); + + lvh::GamepadState state; + state.buttons.set(lvh::GamepadButton::touchpad); + state.battery = lvh::GamepadBattery {.state = lvh::GamepadBatteryState::full, .percentage = 100}; + + const auto report = lvh::reports::pack_input_report(profile, state); + + ASSERT_EQ(report.size(), profile.input_report_size); + EXPECT_EQ(report[0], 0x11); + EXPECT_EQ(report[3], 128); + EXPECT_EQ(report[4], 128); + EXPECT_EQ(report[9], 0x02); + EXPECT_EQ(report[32], 0x1B); + EXPECT_EQ(report[35], 1); + + const auto crc_offset = report.size() - 4U; + const auto expected_crc = test_crc32(std::span {report}.first(crc_offset), test_playstation_crc_seed(0xA1)); EXPECT_EQ(read_u32_le(report, crc_offset), expected_crc); } @@ -214,7 +286,7 @@ TEST(ReportTest, ParsesDualSenseBluetoothOutputReportEvents) { report[47] = 0x22; report[48] = 0x33; const auto crc_offset = report.size() - 4U; - const auto crc = test_crc32(std::span {report}.first(crc_offset), test_dualsense_crc_seed(0xA2)); + const auto crc = test_crc32(std::span {report}.first(crc_offset), test_playstation_crc_seed(0xA2)); report[crc_offset] = static_cast(crc & 0xFFU); report[crc_offset + 1U] = static_cast((crc >> 8U) & 0xFFU); report[crc_offset + 2U] = static_cast((crc >> 16U) & 0xFFU); @@ -231,6 +303,57 @@ TEST(ReportTest, ParsesDualSenseBluetoothOutputReportEvents) { EXPECT_EQ(outputs[2].kind, lvh::GamepadOutputKind::adaptive_triggers); } +TEST(ReportTest, ParsesDualShock4OutputReportEvents) { + const auto profile = lvh::profiles::dualshock4_usb(); + std::vector report(profile.output_report_size, 0); + report[0] = 0x05; + report[1] = 0x03; + report[4] = 0x80; + report[5] = 0x40; + report[6] = 0x11; + report[7] = 0x22; + report[8] = 0x33; + + const auto outputs = lvh::reports::parse_output_reports(profile, report); + + ASSERT_EQ(outputs.size(), 2U); + EXPECT_EQ(outputs[0].kind, lvh::GamepadOutputKind::rumble); + EXPECT_GT(outputs[0].low_frequency_rumble, 0U); + EXPECT_GT(outputs[0].high_frequency_rumble, 0U); + EXPECT_EQ(outputs[1].kind, lvh::GamepadOutputKind::rgb_led); + EXPECT_EQ(outputs[1].red, 0x11); + EXPECT_EQ(outputs[1].green, 0x22); + EXPECT_EQ(outputs[1].blue, 0x33); +} + +TEST(ReportTest, ParsesDualShock4BluetoothOutputReportEvents) { + const auto profile = lvh::profiles::dualshock4_bluetooth(); + std::vector report(profile.output_report_size, 0); + report[0] = 0x11; + report[1] = 0xC0; + report[3] = 0x03; + report[6] = 0x80; + report[7] = 0x40; + report[8] = 0x11; + report[9] = 0x22; + report[10] = 0x33; + const auto crc_offset = report.size() - 4U; + const auto crc = test_crc32(std::span {report}.first(crc_offset), test_playstation_crc_seed(0xA2)); + report[crc_offset] = static_cast(crc & 0xFFU); + report[crc_offset + 1U] = static_cast((crc >> 8U) & 0xFFU); + report[crc_offset + 2U] = static_cast((crc >> 16U) & 0xFFU); + report[crc_offset + 3U] = static_cast((crc >> 24U) & 0xFFU); + + const auto outputs = lvh::reports::parse_output_reports(profile, report); + + ASSERT_EQ(outputs.size(), 2U); + EXPECT_EQ(outputs[0].kind, lvh::GamepadOutputKind::rumble); + EXPECT_EQ(outputs[1].kind, lvh::GamepadOutputKind::rgb_led); + EXPECT_EQ(outputs[1].red, 0x11); + EXPECT_EQ(outputs[1].green, 0x22); + EXPECT_EQ(outputs[1].blue, 0x33); +} + TEST(ReportTest, KeepsUnrecognizedOutputReportsRaw) { const auto rumble_profile = lvh::profiles::xbox_360(); const std::vector wrong_report_id {0x7F, 0x34, 0x12, 0xCD, 0xAB}; diff --git a/tests/unit/test_windows_protocol.cpp b/tests/unit/test_windows_protocol.cpp index 0bbf77d..976c4b9 100644 --- a/tests/unit/test_windows_protocol.cpp +++ b/tests/unit/test_windows_protocol.cpp @@ -76,6 +76,10 @@ TEST(WindowsProtocolTest, MapsBusTypesAndGamepadKinds) { lvh::detail::windows::protocol_gamepad_kind(lvh::GamepadProfileKind::xbox_series), LVH_WINDOWS_GAMEPAD_XBOX_SERIES ); + EXPECT_EQ( + lvh::detail::windows::protocol_gamepad_kind(lvh::GamepadProfileKind::dualshock4), + LVH_WINDOWS_GAMEPAD_DUALSHOCK4 + ); EXPECT_EQ( lvh::detail::windows::protocol_gamepad_kind(lvh::GamepadProfileKind::dualsense), LVH_WINDOWS_GAMEPAD_DUALSENSE