From 15fa5500d6d7a8a3844563de93e8920f87adf1be Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Sun, 19 May 2024 19:45:55 +0100 Subject: [PATCH] fix: implemented get_nodes() for virtual PS5 joypad --- include/inputtino/input.hpp | 2 +- src/c-bindings/joypad_ps5.cpp | 3 +- src/uhid/include/uhid/protected_types.hpp | 1 + src/uhid/joypad_ps5.cpp | 78 +++++++++++++++++++++-- tests/testCAPI.cpp | 8 ++- tests/testJoypads.cpp | 19 +++++- 6 files changed, 101 insertions(+), 10 deletions(-) diff --git a/include/inputtino/input.hpp b/include/inputtino/input.hpp index ac0f559..838bb62 100644 --- a/include/inputtino/input.hpp +++ b/include/inputtino/input.hpp @@ -428,6 +428,6 @@ class PS5Joypad : public Joypad { std::shared_ptr _state; private: - PS5Joypad(); + PS5Joypad(uint16_t vendor_id); }; } // namespace inputtino diff --git a/src/c-bindings/joypad_ps5.cpp b/src/c-bindings/joypad_ps5.cpp index f08a305..eca7736 100644 --- a/src/c-bindings/joypad_ps5.cpp +++ b/src/c-bindings/joypad_ps5.cpp @@ -1,7 +1,8 @@ #include "helpers.hpp" #include -InputtinoPS5Joypad *inputtino_joypad_ps5_create(const InputtinoDeviceDefinition *device, const InputtinoErrorHandler *eh) { +InputtinoPS5Joypad *inputtino_joypad_ps5_create(const InputtinoDeviceDefinition *device, + const InputtinoErrorHandler *eh) { auto joypad_ = inputtino::PS5Joypad::create({ .name = device->name ? device->name : "Inputtino virtual device", .vendor_id = device->vendor_id, diff --git a/src/uhid/include/uhid/protected_types.hpp b/src/uhid/include/uhid/protected_types.hpp index a401f95..39627a7 100644 --- a/src/uhid/include/uhid/protected_types.hpp +++ b/src/uhid/include/uhid/protected_types.hpp @@ -20,6 +20,7 @@ struct PS5JoypadState { unsigned char mac_address[6] = { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF }; + uint16_t vendor_id; uhid::dualsense_input_report_usb current_state; uint8_t touch_points_ids[2] = {0}; diff --git a/src/uhid/joypad_ps5.cpp b/src/uhid/joypad_ps5.cpp index 137ada0..bf549e8 100644 --- a/src/uhid/joypad_ps5.cpp +++ b/src/uhid/joypad_ps5.cpp @@ -1,10 +1,13 @@ #include #include +#include +#include #include +#include +#include #include #include #include -#include namespace inputtino { @@ -115,8 +118,9 @@ void generate_mac_address(PS5JoypadState *state) { } } -PS5Joypad::PS5Joypad() : _state(std::make_shared()) { +PS5Joypad::PS5Joypad(uint16_t vendor_id) : _state(std::make_shared()) { generate_mac_address(this->_state.get()); + this->_state->vendor_id = vendor_id; } PS5Joypad::~PS5Joypad() { @@ -138,7 +142,7 @@ Result PS5Joypad::create(const DeviceDefinition &device) { .country = 0, .report_description = {&uhid::ps5_rdesc[0], &uhid::ps5_rdesc[0] + sizeof(uhid::ps5_rdesc)}}; - auto joypad = PS5Joypad(); + auto joypad = PS5Joypad(device.vendor_id); auto dev = uhid::Device::create(def, [state = joypad._state](uhid_event ev, int fd) { on_uhid_event(state, ev, fd); }); if (dev) { @@ -153,9 +157,73 @@ static int scale_value(int input, int input_start, int input_end, int output_sta return output_start + std::round(slope * (input - input_start)); } +template std::string to_hex(T i) { + std::stringstream stream; + stream << std::hex << std::uppercase << i; + return stream.str(); +} + +std::string mac_to_str(const PS5JoypadState &state) { + std::stringstream stream; + stream << std::hex << (unsigned int)state.mac_address[0] << ":" << (unsigned int)state.mac_address[1] << ":" + << (unsigned int)state.mac_address[2] << ":" << (unsigned int)state.mac_address[3] << ":" + << (unsigned int)state.mac_address[4] << ":" << (unsigned int)state.mac_address[5]; + return stream.str(); +} + +/** + * The trick here is to match the devices under /sys/devices/virtual/misc/uhid/ + * with the MAC address that we've set for the current device + */ std::vector PS5Joypad::get_nodes() const { - // TODO - return std::vector(); + std::vector nodes; + auto base_path = "/sys/devices/virtual/misc/uhid/"; + auto target_mac = mac_to_str(*this->_state); + if (std::filesystem::exists(base_path)) { + auto uhid_entries = std::filesystem::directory_iterator{base_path}; + for (auto uhid_entry : uhid_entries) { + // Here we are looking for a directory that has a name like {BUS_ID}:{VENDOR_ID}:{PRODUCT_ID}.xxxx + // (ex: 0003:054C:0CE6.000D) + auto uhid_candidate_path = uhid_entry.path().filename().string(); + auto target_id = to_hex(this->_state->vendor_id); + if (uhid_entry.is_directory() && uhid_candidate_path.find(target_id) != std::string::npos) { + // Found a match! Let's scan the input devices in that directory + if (std::filesystem::exists(uhid_entry.path() / "input")) { + // ex: /sys/devices/virtual/misc/uhid/0003:054C:0CE6.000D/input/ + auto dev_entries = std::filesystem::directory_iterator{uhid_entry.path() / "input"}; + for (auto dev_entry : dev_entries) { + // Here we only have a match if the "uniq" file inside contains the same MAC address that we've set + if (dev_entry.is_directory()) { + // ex: /sys/devices/virtual/misc/uhid/0003:054C:0CE6.000D/input/input58/uniq + auto dev_uniq_path = dev_entry.path() / "uniq"; + if (std::filesystem::exists(dev_uniq_path)) { + std::ifstream dev_uniq_file{dev_uniq_path}; + std::string line; + std::getline(dev_uniq_file, line); + if (line.find(target_mac) != std::string::npos) { + // Found a match! Let's scan the folders for the corresponding event and js node + auto dev_nodes = std::filesystem::directory_iterator{dev_entry.path()}; + for (auto dev_node : dev_nodes) { + if (dev_node.is_directory() && (dev_node.path().filename().string().rfind("event", 0) == 0 || + dev_node.path().filename().string().rfind("js", 0) == 0)) { + nodes.push_back("/dev/input/" / dev_node.path().filename()); + } + } + } + } else { + fprintf(stderr, "Unable to get joypad nodes, path %s does not exist\n", dev_uniq_path.string().c_str()); + } + } + } + } else { + fprintf(stderr, "Unable to get joypad nodes, path %s does not exist\n", uhid_entry.path().string().c_str()); + } + } + } + } else { + fprintf(stderr, "Unable to get joypad nodes, path %s does not exist\n", base_path); + } + return nodes; } void PS5Joypad::set_pressed_buttons(int pressed) { diff --git a/tests/testCAPI.cpp b/tests/testCAPI.cpp index 7deef64..2c06e26 100644 --- a/tests/testCAPI.cpp +++ b/tests/testCAPI.cpp @@ -181,13 +181,17 @@ TEST_CASE("C Switch API", "[C-API]") { TEST_CASE("C PS5 API", "[C-API]") { InputtinoErrorHandler error_handler = {.eh = [](const char *message, void *_data) { FAIL(message); }, .user_data = nullptr}; - InputtinoDeviceDefinition def = {}; + InputtinoDeviceDefinition def = {.name = "Wolf DualSense (virtual) pad", + .vendor_id = 0x054C, + .product_id = 0x0CE6, + .version = 0x8111}; auto ps_pad = inputtino_joypad_ps5_create(&def, &error_handler); REQUIRE(ps_pad != nullptr); int num_nodes = 0; auto nodes = inputtino_joypad_ps5_get_nodes(ps_pad, &num_nodes); - REQUIRE(num_nodes == 0); // TODO: implement this! + REQUIRE(num_nodes == 5); + REQUIRE_THAT(std::string(nodes[0]), Catch::Matchers::StartsWith("/dev/input/")); { // TODO: test that this actually work inputtino_joypad_ps5_set_pressed_buttons(ps_pad, INPUTTINO_JOYPAD_BTN::A | INPUTTINO_JOYPAD_BTN::B); diff --git a/tests/testJoypads.cpp b/tests/testJoypads.cpp index 0f035c2..049d9d6 100644 --- a/tests/testJoypads.cpp +++ b/tests/testJoypads.cpp @@ -4,7 +4,10 @@ #include #include +using Catch::Matchers::Contains; +using Catch::Matchers::ContainsSubstring; using Catch::Matchers::Equals; +using Catch::Matchers::SizeIs; using Catch::Matchers::WithinAbs; using namespace inputtino; using namespace std::chrono_literals; @@ -89,6 +92,11 @@ TEST_CASE_METHOD(SDLTestsFixture, "PS Joypad", "[SDL]") { std::this_thread::sleep_for(250ms); + auto devices = joypad.get_nodes(); + REQUIRE_THAT(devices, SizeIs(5)); // 3 eventXX and 2 jsYY + REQUIRE_THAT(devices, Contains(ContainsSubstring("/dev/input/event"))); + REQUIRE_THAT(devices, Contains(ContainsSubstring("/dev/input/js"))); + // TODO: seems that I can't force it to use HIDAPI, it's picking up sysjoystick which is lacking features SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI, "1"); SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5, "1"); @@ -103,7 +111,6 @@ TEST_CASE_METHOD(SDLTestsFixture, "PS Joypad", "[SDL]") { REQUIRE(gc); REQUIRE(SDL_GameControllerGetType(gc) == SDL_CONTROLLER_TYPE_PS5); - { // Rumble // Checking for basic capability REQUIRE(SDL_GameControllerHasRumble(gc)); @@ -256,6 +263,11 @@ TEST_CASE_METHOD(SDLTestsFixture, "XBOX Joypad", "[SDL]") { std::this_thread::sleep_for(150ms); + auto devices = joypad.get_nodes(); + REQUIRE_THAT(devices, SizeIs(2)); // 1 eventXX and 1 jsYY + REQUIRE_THAT(devices, Contains(ContainsSubstring("/dev/input/event"))); + REQUIRE_THAT(devices, Contains(ContainsSubstring("/dev/input/js"))); + // Initializing the controller flush_sdl_events(); SDL_GameController *gc = SDL_GameControllerOpen(0); @@ -324,6 +336,11 @@ TEST_CASE_METHOD(SDLTestsFixture, "Nintendo Joypad", "[SDL]") { std::this_thread::sleep_for(150ms); + auto devices = joypad.get_nodes(); + REQUIRE_THAT(devices, SizeIs(2)); // 1 eventXX and 1 jsYY + REQUIRE_THAT(devices, Contains(ContainsSubstring("/dev/input/event"))); + REQUIRE_THAT(devices, Contains(ContainsSubstring("/dev/input/js"))); + // Initializing the controller flush_sdl_events(); SDL_GameController *gc = SDL_GameControllerOpen(0);