diff --git a/CMakeLists.txt b/CMakeLists.txt index 339be46..2546b78 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,6 +74,7 @@ generate_export_header(libinputtino EXPORT_FILE_NAME include/inputtino/${export_ option(BUILD_C_BINDINGS "Build C bindings" OFF) option(LIBINPUTTINO_INSTALL "Generate the install target" OFF) +option(USE_UHID "Add uhid implementation for advanced DualSense features" ON) #---------------------------------------------------------------------------------------------------------------------- # Dependencies @@ -107,12 +108,26 @@ set(PUBLIC_HEADERS include/inputtino/input.h) if (UNIX AND NOT APPLE) - file(GLOB SRC_LIST SRCS src/uinput/*.cpp) - target_sources(libinputtino PRIVATE - ${SRC_LIST} - "src/uinput/joypad_utils.hpp" - "src/uhid/joypad_ps5.cpp") - target_include_directories(libinputtino PUBLIC "src/uinput/include" "src/uhid/include/") + list(APPEND SRC_LIST + "src/uinput/joypad_nintendo.cpp" + "src/uinput/joypad_xbox.cpp" + "src/uinput/keyboard.cpp" + "src/uinput/mouse.cpp" + "src/uinput/pentablet.cpp" + "src/uinput/touchscreen.cpp" + "src/uinput/trackpad.cpp") + + if (USE_UHID) + message("Using uhid implementation for DualSense controller") + list(APPEND SRC_LIST "src/uhid/joypad_ps5.cpp") + target_include_directories(libinputtino PUBLIC "src/uhid/include/") + else () + message("Using uinput implementation for DualSense controller") + list(APPEND SRC_LIST "src/uinput/joypad_ps.cpp") + endif () + + target_sources(libinputtino PRIVATE ${SRC_LIST}) + target_include_directories(libinputtino PUBLIC "src/uinput/include") endif () if (BUILD_C_BINDINGS) diff --git a/src/uhid/include/uhid/uhid.hpp b/src/uhid/include/uhid/uhid.hpp index 009b767..7cc5cb2 100644 --- a/src/uhid/include/uhid/uhid.hpp +++ b/src/uhid/include/uhid/uhid.hpp @@ -115,7 +115,7 @@ inputtino::Result Device::create(const DeviceDefinition &definition, ev.type = UHID_CREATE2, ev.u.create2.bus = definition.bus, ev.u.create2.vendor = definition.vendor, ev.u.create2.product = definition.product, ev.u.create2.version = definition.version, ev.u.create2.country = definition.country, - ev.u.create2.rd_size = static_cast<__u16>(definition.report_description.size()), + ev.u.create2.rd_size = static_cast(definition.report_description.size()), std::copy(definition.report_description.begin(), definition.report_description.end(), ev.u.create2.rd_data); set_c_str(definition.name, ev.u.create2.name); set_c_str(definition.phys, ev.u.create2.phys); @@ -165,4 +165,4 @@ inputtino::Result Device::create(const DeviceDefinition &definition, } } -} // namespace uhid \ No newline at end of file +} // namespace uhid diff --git a/src/uinput/include/inputtino/protected_ps5_types.hpp b/src/uinput/include/inputtino/protected_ps5_types.hpp new file mode 100644 index 0000000..c8f03e7 --- /dev/null +++ b/src/uinput/include/inputtino/protected_ps5_types.hpp @@ -0,0 +1,7 @@ +#pragma once + +#include + +namespace inputtino { +struct PS5JoypadState : BaseJoypadState {}; +} // namespace inputtino \ No newline at end of file diff --git a/src/uinput/joypad_ps.cpp b/src/uinput/joypad_ps.cpp new file mode 100644 index 0000000..d682434 --- /dev/null +++ b/src/uinput/joypad_ps.cpp @@ -0,0 +1,202 @@ +#include "joypad_utils.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace inputtino { + +std::vector PS5Joypad::get_nodes() const { + std::vector nodes; + + if (auto joy = _state->joy.get()) { + auto additional_nodes = get_child_dev_nodes(joy); + nodes.insert(nodes.end(), additional_nodes.begin(), additional_nodes.end()); + } + + return nodes; +} + +Result create_ps_controller(const DeviceDefinition &device) { + libevdev *dev = libevdev_new(); + libevdev_uinput *uidev; + + libevdev_set_name(dev, device.name.c_str()); + libevdev_set_id_vendor(dev, device.vendor_id); + libevdev_set_id_product(dev, device.product_id); + libevdev_set_id_version(dev, device.version); + libevdev_set_id_bustype(dev, BUS_USB); + + libevdev_enable_event_type(dev, EV_KEY); + libevdev_enable_event_code(dev, EV_KEY, BTN_WEST, nullptr); + libevdev_enable_event_code(dev, EV_KEY, BTN_EAST, nullptr); + libevdev_enable_event_code(dev, EV_KEY, BTN_NORTH, nullptr); + libevdev_enable_event_code(dev, EV_KEY, BTN_SOUTH, nullptr); + libevdev_enable_event_code(dev, EV_KEY, BTN_THUMBL, nullptr); + libevdev_enable_event_code(dev, EV_KEY, BTN_THUMBR, nullptr); + libevdev_enable_event_code(dev, EV_KEY, BTN_TR, nullptr); + libevdev_enable_event_code(dev, EV_KEY, BTN_TL, nullptr); + libevdev_enable_event_code(dev, EV_KEY, BTN_TR2, nullptr); + libevdev_enable_event_code(dev, EV_KEY, BTN_TL2, nullptr); + libevdev_enable_event_code(dev, EV_KEY, BTN_SELECT, nullptr); + libevdev_enable_event_code(dev, EV_KEY, BTN_MODE, nullptr); + libevdev_enable_event_code(dev, EV_KEY, BTN_START, nullptr); + + libevdev_enable_event_type(dev, EV_ABS); + + input_absinfo dpad{0, -1, 1, 0, 0, 0}; + libevdev_enable_event_code(dev, EV_ABS, ABS_HAT0Y, &dpad); + libevdev_enable_event_code(dev, EV_ABS, ABS_HAT0X, &dpad); + + // see: https://github.com/games-on-whales/wolf/issues/56 + input_absinfo stick{0, -32768, 32767, 16, 128, 0}; + libevdev_enable_event_code(dev, EV_ABS, ABS_X, &stick); + libevdev_enable_event_code(dev, EV_ABS, ABS_RX, &stick); + libevdev_enable_event_code(dev, EV_ABS, ABS_Y, &stick); + libevdev_enable_event_code(dev, EV_ABS, ABS_RY, &stick); + + input_absinfo trigger{0, 0, 255, 0, 0, 0}; + libevdev_enable_event_code(dev, EV_ABS, ABS_Z, &trigger); + libevdev_enable_event_code(dev, EV_ABS, ABS_RZ, &trigger); + + libevdev_enable_event_type(dev, EV_FF); + libevdev_enable_event_code(dev, EV_FF, FF_RUMBLE, nullptr); + libevdev_enable_event_code(dev, EV_FF, FF_CONSTANT, nullptr); + libevdev_enable_event_code(dev, EV_FF, FF_PERIODIC, nullptr); + libevdev_enable_event_code(dev, EV_FF, FF_SINE, nullptr); + libevdev_enable_event_code(dev, EV_FF, FF_RAMP, nullptr); + libevdev_enable_event_code(dev, EV_FF, FF_GAIN, nullptr); + + auto err = libevdev_uinput_create_from_device(dev, LIBEVDEV_UINPUT_OPEN_MANAGED, &uidev); + libevdev_free(dev); + if (err != 0) { + return Error(strerror(-err)); + } + + return libevdev_uinput_ptr{uidev, ::libevdev_uinput_destroy}; +} + +PS5Joypad::PS5Joypad(uint16_t vendor_id) : _state(std::make_shared()) {} + +PS5Joypad::~PS5Joypad() { + if (_state) { + _state->stop_listening_events = true; + if (_state->joy.get() != nullptr && _state->events_thread.joinable()) { + _state->events_thread.join(); + } + } +} + +Result PS5Joypad::create(const DeviceDefinition &device) { + auto joy_el = create_ps_controller(device); + if (!joy_el) { + return Error(joy_el.getErrorMessage()); + } + + PS5Joypad joypad(0); + joypad._state->joy = std::move(*joy_el); + + auto event_thread = std::thread(event_listener, joypad._state); + joypad._state->events_thread = std::move(event_thread); + joypad._state->events_thread.detach(); + + return joypad; +} + +void PS5Joypad::set_pressed_buttons(unsigned int newly_pressed) { + // Button flags that have been changed between current and prev + auto bf_changed = newly_pressed ^ this->_state->currently_pressed_btns; + // Button flags that are only part of the new packet + auto bf_new = newly_pressed; + if (auto controller = this->_state->joy.get()) { + + if (bf_changed) { + if ((DPAD_UP | DPAD_DOWN) & bf_changed) { + int button_state = bf_new & DPAD_UP ? -1 : (bf_new & DPAD_DOWN ? 1 : 0); + + libevdev_uinput_write_event(controller, EV_ABS, ABS_HAT0Y, button_state); + } + + if ((DPAD_LEFT | DPAD_RIGHT) & bf_changed) { + int button_state = bf_new & DPAD_LEFT ? -1 : (bf_new & DPAD_RIGHT ? 1 : 0); + + libevdev_uinput_write_event(controller, EV_ABS, ABS_HAT0X, button_state); + } + + if (START & bf_changed) + libevdev_uinput_write_event(controller, EV_KEY, BTN_START, bf_new & START ? 1 : 0); + if (BACK & bf_changed) + libevdev_uinput_write_event(controller, EV_KEY, BTN_SELECT, bf_new & BACK ? 1 : 0); + if (LEFT_STICK & bf_changed) + libevdev_uinput_write_event(controller, EV_KEY, BTN_THUMBL, bf_new & LEFT_STICK ? 1 : 0); + if (RIGHT_STICK & bf_changed) + libevdev_uinput_write_event(controller, EV_KEY, BTN_THUMBR, bf_new & RIGHT_STICK ? 1 : 0); + if (LEFT_BUTTON & bf_changed) + libevdev_uinput_write_event(controller, EV_KEY, BTN_TL, bf_new & LEFT_BUTTON ? 1 : 0); + if (RIGHT_BUTTON & bf_changed) + libevdev_uinput_write_event(controller, EV_KEY, BTN_TR, bf_new & RIGHT_BUTTON ? 1 : 0); + if (HOME & bf_changed) + libevdev_uinput_write_event(controller, EV_KEY, BTN_MODE, bf_new & HOME ? 1 : 0); + if (A & bf_changed) + libevdev_uinput_write_event(controller, EV_KEY, BTN_SOUTH, bf_new & A ? 1 : 0); + if (B & bf_changed) + libevdev_uinput_write_event(controller, EV_KEY, BTN_EAST, bf_new & B ? 1 : 0); + if (X & bf_changed) + libevdev_uinput_write_event(controller, EV_KEY, BTN_WEST, bf_new & X ? 1 : 0); + if (Y & bf_changed) + libevdev_uinput_write_event(controller, EV_KEY, BTN_NORTH, bf_new & Y ? 1 : 0); + } + + libevdev_uinput_write_event(controller, EV_SYN, SYN_REPORT, 0); + } + this->_state->currently_pressed_btns = bf_new; +} + +void PS5Joypad::set_stick(Joypad::STICK_POSITION stick_type, short x, short y) { + if (auto controller = this->_state->joy.get()) { + if (stick_type == LS) { + libevdev_uinput_write_event(controller, EV_ABS, ABS_X, x); + libevdev_uinput_write_event(controller, EV_ABS, ABS_Y, -y); + } else { + libevdev_uinput_write_event(controller, EV_ABS, ABS_RX, x); + libevdev_uinput_write_event(controller, EV_ABS, ABS_RY, -y); + } + + libevdev_uinput_write_event(controller, EV_SYN, SYN_REPORT, 0); + } +} + +void PS5Joypad::set_triggers(int16_t left, int16_t right) { + if (auto controller = this->_state->joy.get()) { + if (left > 0) { + libevdev_uinput_write_event(controller, EV_ABS, ABS_Z, left); + } else { + libevdev_uinput_write_event(controller, EV_ABS, ABS_Z, left); + } + + if (right > 0) { + libevdev_uinput_write_event(controller, EV_ABS, ABS_RZ, right); + } else { + libevdev_uinput_write_event(controller, EV_ABS, ABS_RZ, right); + } + + libevdev_uinput_write_event(controller, EV_SYN, SYN_REPORT, 0); + } +} + +void PS5Joypad::set_on_rumble(const std::function &callback) { + this->_state->on_rumble = callback; +} + +// Followings aren't supported when not using the UHID implementation +void PS5Joypad::place_finger(int finger_nr, uint16_t x, uint16_t y) {} +void PS5Joypad::release_finger(int finger_nr) {} +void PS5Joypad::set_motion(MOTION_TYPE type, float x, float y, float z) {} +void PS5Joypad::set_battery(BATTERY_STATE state, int percentage) {} +void PS5Joypad::set_on_led(const std::function &callback) {} +void PS5Joypad::set_on_trigger_effect(const std::function &callback) {} + +} // namespace inputtino diff --git a/src/uinput/joypad_utils.hpp b/src/uinput/joypad_utils.hpp index 6822641..412f950 100644 --- a/src/uinput/joypad_utils.hpp +++ b/src/uinput/joypad_utils.hpp @@ -100,8 +100,8 @@ static ActiveRumbleEffect create_rumble_effect(const ff_effect &effect) { ActiveRumbleEffect r_effect{ .start_point = std::chrono::steady_clock::time_point::min(), .end_point = std::chrono::steady_clock::time_point::min(), - .length = std::chrono::milliseconds{std::clamp(effect.replay.length, (__u16)0, (__u16)32767)}, - .delay = std::chrono::milliseconds{std::clamp(effect.replay.delay, (__u16)0, (__u16)32767)}, + .length = std::chrono::milliseconds{std::clamp(effect.replay.length, (std::uint16_t)0, (std::uint16_t)32767)}, + .delay = std::chrono::milliseconds{std::clamp(effect.replay.delay, (std::uint16_t)0, (std::uint16_t)32767)}, .envelope = {}}; switch (effect.type) { case FF_CONSTANT: diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2c370d7..a7247fd 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -23,6 +23,7 @@ if (UNIX AND NOT APPLE) if (SDL_CUSTOM_SRC) SET(SDL_TEST OFF) SET(SDL_HIDAPI_JOYSTICK ON) + SET(SDL_PIPEWIRE OFF) add_subdirectory(${SDL_CUSTOM_SRC} ${CMAKE_CURRENT_BINARY_DIR}/sdl EXCLUDE_FROM_ALL) else () find_package(SDL2 REQUIRED CONFIG REQUIRED COMPONENTS SDL2) @@ -35,6 +36,10 @@ if (UNIX AND NOT APPLE) list(APPEND SRC_LIST "testJoypads.cpp" "testLibinput.cpp") + + if (USE_UHID) + list(APPEND SRC_LIST "testUHID.cpp") + endif () endif () endif () diff --git a/tests/testCAPI.cpp b/tests/testCAPI.cpp index eb7874e..609f086 100644 --- a/tests/testCAPI.cpp +++ b/tests/testCAPI.cpp @@ -191,7 +191,7 @@ TEST_CASE("C PS5 API", "[C-API]") { std::this_thread::sleep_for(std::chrono::milliseconds(50)); int num_nodes = 0; auto nodes = inputtino_joypad_ps5_get_nodes(ps_pad, &num_nodes); - REQUIRE(num_nodes == 5); + REQUIRE(num_nodes >= 2); REQUIRE_THAT(std::string(nodes[0]), Catch::Matchers::StartsWith("/dev/input/")); { // TODO: test that this actually work diff --git a/tests/testJoypads.cpp b/tests/testJoypads.cpp index 57cf62b..01f5361 100644 --- a/tests/testJoypads.cpp +++ b/tests/testJoypads.cpp @@ -1,12 +1,10 @@ #include "catch2/catch_all.hpp" -#include #include #include #include #include #include #include -#include using Catch::Matchers::Contains; using Catch::Matchers::ContainsSubstring; @@ -16,7 +14,7 @@ using Catch::Matchers::WithinAbs; using namespace inputtino; using namespace std::chrono_literals; -void flush_sdl_events() { +static void flush_sdl_events() { SDL_JoystickUpdate(); SDL_Event event; while (SDL_PollEvent(&event) != 0) { @@ -73,7 +71,7 @@ class SDLTestsFixture { flush_sdl_events(); \ REQUIRE(SDL_GameControllerGetButton(gc, SDL_BTN) == 1); -void test_buttons(SDL_GameController *gc, Joypad &joypad) { +static void test_buttons(SDL_GameController *gc, Joypad &joypad) { SDL_TEST_BUTTON(Joypad::DPAD_UP, SDL_CONTROLLER_BUTTON_DPAD_UP) SDL_TEST_BUTTON(Joypad::DPAD_DOWN, SDL_CONTROLLER_BUTTON_DPAD_DOWN) SDL_TEST_BUTTON(Joypad::DPAD_LEFT, SDL_CONTROLLER_BUTTON_DPAD_LEFT) @@ -110,41 +108,17 @@ void test_buttons(SDL_GameController *gc, Joypad &joypad) { REQUIRE(SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_Y) == 1); } -/** - * @param power_supply_path (ex: - * "/sys/devices/virtual/misc/uhid/0003:054C:0CE6.0016/power_supply/ps-controller-battery-00:21:c1:75:88:38/") - * @return a pair of as read from the system - */ -std::pair get_system_battery(const std::filesystem::path &power_supply_path) { - // It's fairly simple, we have to read two files: capacity and status - std::ifstream capacity_file(power_supply_path / "capacity"); - std::ifstream status_file(power_supply_path / "status"); - if (!capacity_file.is_open() || !status_file.is_open()) { - return {0, "Unknown"}; - } - int capacity = 0; - std::string status; - capacity_file >> capacity; - status_file >> status; - return {capacity, status}; -} - -TEST_CASE_METHOD(SDLTestsFixture, "PS Joypad", "[SDL],[PS]") { +TEST_CASE_METHOD(SDLTestsFixture, "XBOX Joypad", "[SDL]") { // Create the controller - auto joypad = std::move(*PS5Joypad::create()); + auto joypad = std::move(*XboxOneJoypad::create()); - std::this_thread::sleep_for(50ms); + std::this_thread::sleep_for(150ms); auto devices = joypad.get_nodes(); - REQUIRE_THAT(devices, SizeIs(5)); // 3 eventXX and 2 jsYY + 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"))); - // 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"); - SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1"); - SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_PLAYER_LED, "1"); // Initializing the controller flush_sdl_events(); SDL_GameController *gc = SDL_GameControllerOpen(0); @@ -152,29 +126,29 @@ TEST_CASE_METHOD(SDLTestsFixture, "PS Joypad", "[SDL],[PS]") { WARN(SDL_GetError()); } REQUIRE(gc); + REQUIRE(SDL_GameControllerGetType(gc) == SDL_CONTROLLER_TYPE_XBOXONE); + // Checking for basic joypad capabilities + REQUIRE(SDL_GameControllerHasRumble(gc)); - REQUIRE(SDL_GameControllerGetType(gc) == SDL_CONTROLLER_TYPE_PS5); + test_buttons(gc, joypad); { // Rumble // Checking for basic capability REQUIRE(SDL_GameControllerHasRumble(gc)); - auto rumble_data = std::make_shared>(0, 0); + auto rumble_data = std::make_shared>(); joypad.set_on_rumble([rumble_data](int low_freq, int high_freq) { - if (rumble_data->first == 0) - rumble_data->first = low_freq; - if (rumble_data->second == 0) - rumble_data->second = high_freq; + rumble_data->first = low_freq; + rumble_data->second = high_freq; }); // When debugging this, bear in mind that SDL will send max duration here // https://github.com/libsdl-org/SDL/blob/da8fc70a83cf6b76d5ea75c39928a7961bd163d3/src/joystick/linux/SDL_sysjoystick.c#L1628 - SDL_GameControllerRumble(gc, 0xFF00, 0xF00F, 100); + SDL_GameControllerRumble(gc, 100, 200, 100); std::this_thread::sleep_for(30ms); // wait for the effect to be picked up - REQUIRE(rumble_data->first == 0x7f7f); - REQUIRE(rumble_data->second == 0x7878); + REQUIRE(rumble_data->first == 100); + REQUIRE(rumble_data->second == 200); } - test_buttons(gc, joypad); { // Sticks REQUIRE(SDL_GameControllerHasAxis(gc, SDL_CONTROLLER_AXIS_LEFTX)); REQUIRE(SDL_GameControllerHasAxis(gc, SDL_CONTROLLER_AXIS_LEFTY)); @@ -185,281 +159,31 @@ TEST_CASE_METHOD(SDLTestsFixture, "PS Joypad", "[SDL],[PS]") { joypad.set_stick(Joypad::LS, 1000, 2000); flush_sdl_events(); - REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTX) == 899); - REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTY) == -1928); + REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTX) == 1000); + REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTY) == -2000); joypad.set_stick(Joypad::RS, 1000, 2000); flush_sdl_events(); - REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTX) == 899); - REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTY) == -1928); - - joypad.set_stick(Joypad::RS, -16384, -32768); - flush_sdl_events(); - REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTX) == -16320); - REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTY) == 32767); + REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTX) == 1000); + REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTY) == -2000); - joypad.set_triggers(125, 255); + joypad.set_triggers(10, 20); flush_sdl_events(); - REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_TRIGGERLEFT) == 16062); - REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_TRIGGERRIGHT) == 32767); + REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_TRIGGERLEFT) == 1284); + REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_TRIGGERRIGHT) == 2569); joypad.set_triggers(0, 0); flush_sdl_events(); REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_TRIGGERLEFT) == 0); REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_TRIGGERRIGHT) == 0); } - { // test acceleration - REQUIRE(SDL_GameControllerHasSensor(gc, SDL_SENSOR_ACCEL)); - if (SDL_GameControllerSetSensorEnabled(gc, SDL_SENSOR_ACCEL, SDL_TRUE) != 0) { - WARN(SDL_GetError()); - } - - std::array acceleration_data = {9.8f, 0.0f, 20.0f}; - joypad.set_motion(inputtino::PS5Joypad::ACCELERATION, - acceleration_data[0], - acceleration_data[1], - acceleration_data[2]); - SDL_GameControllerUpdate(); - SDL_SensorUpdate(); - SDL_Event event; - while (SDL_PollEvent(&event) != 0) { - if (event.type == SDL_CONTROLLERSENSORUPDATE) { - break; - } - } - REQUIRE(event.type == SDL_CONTROLLERSENSORUPDATE); - REQUIRE(event.csensor.sensor == SDL_SENSOR_ACCEL); - REQUIRE_THAT(event.csensor.data[0], WithinAbs(acceleration_data[0], 0.9f)); - REQUIRE_THAT(event.csensor.data[1], WithinAbs(acceleration_data[1], 0.9f)); - REQUIRE_THAT(event.csensor.data[2], WithinAbs(acceleration_data[2], 0.9f)); - flush_sdl_events(); - - // Now lets test the negatives - acceleration_data = {-9.8f, -0.0f, -20.0f}; - joypad.set_motion(inputtino::PS5Joypad::ACCELERATION, - acceleration_data[0], - acceleration_data[1], - acceleration_data[2]); - SDL_GameControllerUpdate(); - SDL_SensorUpdate(); - while (SDL_PollEvent(&event) != 0) { - if (event.type == SDL_CONTROLLERSENSORUPDATE) { - break; - } - } - REQUIRE(event.type == SDL_CONTROLLERSENSORUPDATE); - REQUIRE(event.csensor.sensor == SDL_SENSOR_ACCEL); - REQUIRE_THAT(event.csensor.data[0], WithinAbs(acceleration_data[0], 0.9f)); - REQUIRE_THAT(event.csensor.data[1], WithinAbs(acceleration_data[1], 0.9f)); - REQUIRE_THAT(event.csensor.data[2], WithinAbs(acceleration_data[2], 0.9f)); - flush_sdl_events(); - } - { // test gyro - REQUIRE(SDL_GameControllerHasSensor(gc, SDL_SENSOR_GYRO)); - if (SDL_GameControllerSetSensorEnabled(gc, SDL_SENSOR_GYRO, SDL_TRUE) != 0) { - WARN(SDL_GetError()); - } - - std::array gyro_data = {0.0f, - M_PI_2f, // half a turn (180 degrees) - M_PIf}; // full turn (360 degrees) - joypad.set_motion(inputtino::PS5Joypad::GYROSCOPE, gyro_data[0], gyro_data[1], gyro_data[2]); - std::this_thread::sleep_for(10ms); - - SDL_GameControllerUpdate(); - SDL_SensorUpdate(); - SDL_Event event; - while (SDL_PollEvent(&event) != 0) { - if (event.type == SDL_CONTROLLERSENSORUPDATE) { - break; - } - } - REQUIRE(event.type == SDL_CONTROLLERSENSORUPDATE); - REQUIRE(event.csensor.sensor == SDL_SENSOR_GYRO); - REQUIRE_THAT(event.csensor.data[0], WithinAbs(gyro_data[0], 0.01f)); - REQUIRE_THAT(event.csensor.data[1], WithinAbs(gyro_data[1], 0.01f)); - REQUIRE_THAT(event.csensor.data[2], WithinAbs(gyro_data[2], 0.01f)); - flush_sdl_events(); - - // Now let's try the negatives - gyro_data = { - -0.0f, - -M_PI_2f, // half a turn (180 degrees) - -M_PIf // full turn (360 degrees) - }; - joypad.set_motion(inputtino::PS5Joypad::GYROSCOPE, gyro_data[0], gyro_data[1], gyro_data[2]); - std::this_thread::sleep_for(10ms); - - SDL_GameControllerUpdate(); - SDL_SensorUpdate(); - while (SDL_PollEvent(&event) != 0) { - if (event.type == SDL_CONTROLLERSENSORUPDATE) { - break; - } - } - REQUIRE(event.type == SDL_CONTROLLERSENSORUPDATE); - REQUIRE(event.csensor.sensor == SDL_SENSOR_GYRO); - REQUIRE_THAT(event.csensor.data[0], WithinAbs(gyro_data[0], 0.01f)); - REQUIRE_THAT(event.csensor.data[1], WithinAbs(gyro_data[1], 0.01f)); - REQUIRE_THAT(event.csensor.data[2], WithinAbs(gyro_data[2], 0.01f)); - flush_sdl_events(); - - // Try out problematic values from https://github.com/LizardByte/Sunshine/issues/3247 - gyro_data = {-32769.0f, 32769.0f, -0.0004124999977648258f}; - joypad.set_motion(inputtino::PS5Joypad::GYROSCOPE, gyro_data[0], gyro_data[1], gyro_data[2]); - std::this_thread::sleep_for(10ms); - - SDL_GameControllerUpdate(); - SDL_SensorUpdate(); - while (SDL_PollEvent(&event) != 0) { - if (event.type == SDL_CONTROLLERSENSORUPDATE) { - break; - } - } - REQUIRE(event.type == SDL_CONTROLLERSENSORUPDATE); - REQUIRE(event.csensor.sensor == SDL_SENSOR_GYRO); - REQUIRE_THAT(event.csensor.data[0], WithinAbs(-28.59546f, 0.01f)); - REQUIRE_THAT(event.csensor.data[1], WithinAbs(28.59546f, 0.01f)); - REQUIRE_THAT(event.csensor.data[2], WithinAbs(0.0f, 0.01f)); - flush_sdl_events(); - } - - { // LED TODO: seems that this only works after some gyro/acceleration data is sent - REQUIRE(SDL_GameControllerHasLED(gc)); - struct LED { - int r; - int g; - int b; - }; - auto led_data = std::make_shared(); - joypad.set_on_led([led_data](int r, int g, int b) { - led_data->r = r; - led_data->g = g; - led_data->b = b; - }); - REQUIRE(SDL_GameControllerSetLED(gc, 50, 100, 150) == 0); - std::this_thread::sleep_for(20ms); // wait for the effect to be picked up - REQUIRE(led_data->r == 50); - REQUIRE(led_data->g == 100); - REQUIRE(led_data->b == 150); - } - - { // Test touchpad - REQUIRE(SDL_GameControllerGetNumTouchpads(gc) == 1); - REQUIRE(SDL_GameControllerGetNumTouchpadFingers(gc, 0) == 2); - - // TODO: test these values with SDL - joypad.place_finger(0, 1920, 1080); - joypad.place_finger(1, 1920, 1080); - joypad.release_finger(0); - joypad.release_finger(1); - } - - { // Test battery - auto joy = SDL_GameControllerGetJoystick(gc); - REQUIRE(SDL_JoystickCurrentPowerLevel(joy) == SDL_JOYSTICK_POWER_FULL); - - auto base_path = - std::filesystem::path( - joypad.get_sys_nodes()[0]) // "/sys/devices/virtual/misc/uhid/0003:054C:0CE6.0017/input/input123" - .parent_path() // "/sys/devices/virtual/misc/uhid/0003:054C:0CE6.0017/input/" - .parent_path() // "/sys/devices/virtual/misc/uhid/0003:054C:0CE6.0017/" - / "power_supply" // "/sys/devices/virtual/misc/uhid/0003:054C:0CE6.0017/power_supply" - / ("ps-controller-battery-" + joypad.get_mac_address()); - REQUIRE(std::filesystem::exists(base_path)); - - { // Defaults to full if nothing is set - auto [capacity, status] = get_system_battery(base_path); - REQUIRE(capacity == 100); - REQUIRE(status == "Full"); - } - - { - joypad.set_battery(inputtino::PS5Joypad::BATTERY_CHARGHING, 80); - auto [capacity, status] = get_system_battery(base_path); - REQUIRE(capacity == 85); - REQUIRE(status == "Charging"); - } - - { - joypad.set_battery(inputtino::PS5Joypad::BATTERY_CHARGHING, 10); - auto [capacity, status] = get_system_battery(base_path); - REQUIRE(capacity == 15); - REQUIRE(status == "Charging"); - } - - { - joypad.set_battery(inputtino::PS5Joypad::BATTERY_DISCHARGING, 75); - auto [capacity, status] = get_system_battery(base_path); - REQUIRE(capacity == 75); - REQUIRE(status == "Discharging"); - } - - { - joypad.set_battery(inputtino::PS5Joypad::BATTERY_FULL, 100); - auto [capacity, status] = get_system_battery(base_path); - REQUIRE(capacity == 100); - REQUIRE(status == "Full"); - } - } - - // Adaptive triggers aren't directly supported by SDL - // see:https://github.com/libsdl-org/SDL/issues/5125#issuecomment-1204261666 - // see: HIDAPI_DriverPS5_RumbleJoystickTriggers() - // but we can send custom data to the device, the following code is adapted from - // https://github.com/libsdl-org/SDL/blob/d66483dfccfcdc4e03f719e318c7a76f963f22d9/test/testcontroller.c#L235-L255 - { - auto trigger_event = std::make_shared(); - joypad.set_on_trigger_effect([trigger_event](const PS5Joypad::TriggerEffect &effect) { *trigger_event = effect; }); - - /* Resistance and vibration when trigger is pulled */ - uint8_t left_effect_type = 0x06; - uint8_t left_effect[10] = {15, 63, 128, 0, 0, 0, 0, 0, 0, 0}; - /* Constant resistance across entire trigger pull */ - uint8_t right_effect_type = 0x01; - uint8_t right_effect[10] = {0, 110, 0, 0, 0, 0, 0, 0, 0, 0}; - - uhid::dualsense_output_report_common state = {}; - SDL_zero(state); - state.valid_flag0 |= (uhid::RIGHT_TRIGGER_EFFECT); - state.right_trigger_effect_type = right_effect_type; - SDL_memcpy(state.right_trigger_effect, right_effect, sizeof(right_effect)); - state.left_trigger_effect_type = left_effect_type; - SDL_memcpy(state.left_trigger_effect, left_effect, sizeof(left_effect)); - SDL_GameControllerSendEffect(gc, &state, sizeof(state)); - - std::this_thread::sleep_for(15ms); - flush_sdl_events(); - REQUIRE(trigger_event->event_flags == uhid::RIGHT_TRIGGER_EFFECT); - REQUIRE(trigger_event->type_left == left_effect_type); - REQUIRE(trigger_event->type_right == right_effect_type); - REQUIRE(std::equal(std::begin(trigger_event->left), std::end(trigger_event->left), std::begin(left_effect))); - REQUIRE(std::equal(std::begin(trigger_event->right), std::end(trigger_event->right), std::begin(right_effect))); - } - - { // Test creating a second device - REQUIRE(SDL_NumJoysticks() == 1); - auto joypad2 = std::move(*PS5Joypad::create()); - std::this_thread::sleep_for(50ms); - - auto devices2 = joypad2.get_nodes(); - REQUIRE_THAT(devices2, SizeIs(5)); // 3 eventXX and 2 jsYY - REQUIRE_THAT(devices2, Contains(ContainsSubstring("/dev/input/event"))); - REQUIRE_THAT(devices2, Contains(ContainsSubstring("/dev/input/js"))); - - flush_sdl_events(); - REQUIRE(SDL_NumJoysticks() == 2); - SDL_GameController *gc2 = SDL_GameControllerOpen(1); - REQUIRE(SDL_GameControllerGetType(gc2) == SDL_CONTROLLER_TYPE_PS5); - SDL_GameControllerClose(gc2); - } SDL_GameControllerClose(gc); } -TEST_CASE_METHOD(SDLTestsFixture, "XBOX Joypad", "[SDL]") { +TEST_CASE_METHOD(SDLTestsFixture, "Nintendo Joypad", "[SDL]") { // Create the controller - auto joypad = std::move(*XboxOneJoypad::create()); + auto joypad = std::move(*SwitchJoypad::create()); std::this_thread::sleep_for(150ms); @@ -475,9 +199,7 @@ TEST_CASE_METHOD(SDLTestsFixture, "XBOX Joypad", "[SDL]") { WARN(SDL_GetError()); } REQUIRE(gc); - REQUIRE(SDL_GameControllerGetType(gc) == SDL_CONTROLLER_TYPE_XBOXONE); - // Checking for basic joypad capabilities - REQUIRE(SDL_GameControllerHasRumble(gc)); + REQUIRE(SDL_GameControllerGetType(gc) == SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO); test_buttons(gc, joypad); { // Rumble @@ -498,6 +220,8 @@ TEST_CASE_METHOD(SDLTestsFixture, "XBOX Joypad", "[SDL]") { REQUIRE(rumble_data->second == 200); } + SDL_TEST_BUTTON(Joypad::MISC_FLAG, SDL_CONTROLLER_BUTTON_MISC1); + { // Sticks REQUIRE(SDL_GameControllerHasAxis(gc, SDL_CONTROLLER_AXIS_LEFTX)); REQUIRE(SDL_GameControllerHasAxis(gc, SDL_CONTROLLER_AXIS_LEFTY)); @@ -516,10 +240,11 @@ TEST_CASE_METHOD(SDLTestsFixture, "XBOX Joypad", "[SDL]") { REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTX) == 1000); REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTY) == -2000); + // Nintendo ONLY: triggers are buttons, so it can only be MAX or 0 joypad.set_triggers(10, 20); flush_sdl_events(); - REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_TRIGGERLEFT) == 1284); - REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_TRIGGERRIGHT) == 2569); + REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_TRIGGERLEFT) == 32767); + REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_TRIGGERRIGHT) == 32767); joypad.set_triggers(0, 0); flush_sdl_events(); @@ -530,14 +255,14 @@ TEST_CASE_METHOD(SDLTestsFixture, "XBOX Joypad", "[SDL]") { SDL_GameControllerClose(gc); } -TEST_CASE_METHOD(SDLTestsFixture, "Nintendo Joypad", "[SDL]") { +TEST_CASE_METHOD(SDLTestsFixture, "PS Joypad (basic)", "[SDL],[PS]") { // Create the controller - auto joypad = std::move(*SwitchJoypad::create()); + auto joypad = std::move(*PS5Joypad::create()); - std::this_thread::sleep_for(150ms); + std::this_thread::sleep_for(50ms); auto devices = joypad.get_nodes(); - REQUIRE_THAT(devices, SizeIs(2)); // 1 eventXX and 1 jsYY + REQUIRE_THAT(devices, SizeIs(2)); REQUIRE_THAT(devices, Contains(ContainsSubstring("/dev/input/event"))); REQUIRE_THAT(devices, Contains(ContainsSubstring("/dev/input/js"))); @@ -548,29 +273,29 @@ TEST_CASE_METHOD(SDLTestsFixture, "Nintendo Joypad", "[SDL]") { WARN(SDL_GetError()); } REQUIRE(gc); - REQUIRE(SDL_GameControllerGetType(gc) == SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO); - test_buttons(gc, joypad); + REQUIRE(SDL_GameControllerGetType(gc) == SDL_CONTROLLER_TYPE_PS5); { // Rumble // Checking for basic capability REQUIRE(SDL_GameControllerHasRumble(gc)); - auto rumble_data = std::make_shared>(); + auto rumble_data = std::make_shared>(0, 0); joypad.set_on_rumble([rumble_data](int low_freq, int high_freq) { - rumble_data->first = low_freq; - rumble_data->second = high_freq; + if (rumble_data->first == 0) + rumble_data->first = low_freq; + if (rumble_data->second == 0) + rumble_data->second = high_freq; }); // When debugging this, bear in mind that SDL will send max duration here // https://github.com/libsdl-org/SDL/blob/da8fc70a83cf6b76d5ea75c39928a7961bd163d3/src/joystick/linux/SDL_sysjoystick.c#L1628 - SDL_GameControllerRumble(gc, 100, 200, 100); + SDL_GameControllerRumble(gc, 0xFF00, 0xF00F, 100); std::this_thread::sleep_for(30ms); // wait for the effect to be picked up - REQUIRE(rumble_data->first == 100); - REQUIRE(rumble_data->second == 200); + REQUIRE(rumble_data->first == 0xff00); + REQUIRE(rumble_data->second == 0xF00F); } - SDL_TEST_BUTTON(Joypad::MISC_FLAG, SDL_CONTROLLER_BUTTON_MISC1); - + test_buttons(gc, joypad); { // Sticks REQUIRE(SDL_GameControllerHasAxis(gc, SDL_CONTROLLER_AXIS_LEFTX)); REQUIRE(SDL_GameControllerHasAxis(gc, SDL_CONTROLLER_AXIS_LEFTY)); @@ -586,13 +311,17 @@ TEST_CASE_METHOD(SDLTestsFixture, "Nintendo Joypad", "[SDL]") { joypad.set_stick(Joypad::RS, 1000, 2000); flush_sdl_events(); - REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTX) == 1000); - REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTY) == -2000); + REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTX) == 1000); + REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTY) == -2000); - // Nintendo ONLY: triggers are buttons, so it can only be MAX or 0 - joypad.set_triggers(10, 20); + joypad.set_stick(Joypad::RS, -16384, -32768); flush_sdl_events(); - REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_TRIGGERLEFT) == 32767); + REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTX) == -16384); + REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTY) == 32767); + + joypad.set_triggers(125, 255); + flush_sdl_events(); + REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_TRIGGERLEFT) == 16062); REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_TRIGGERRIGHT) == 32767); joypad.set_triggers(0, 0); @@ -603,14 +332,3 @@ TEST_CASE_METHOD(SDLTestsFixture, "Nintendo Joypad", "[SDL]") { SDL_GameControllerClose(gc); } - -TEST_CASE("Bluetooth CRC32", "[PS]") { - std::string buffer = "123456789"; - auto crc = CRC32(reinterpret_cast(buffer.data()), buffer.length()); - REQUIRE(crc == 0xcbf43926); // https://crccalc.com/?crc=123456789&method=CRC-32/ISO-HDLC&datatype=ascii&outtype=hex - - unsigned char PS_INPUT_CRC32_SEED = 0xA1; - auto crc2 = CRC32(&PS_INPUT_CRC32_SEED, 1, 0xFFFFFFFF); - crc2 = CRC32(reinterpret_cast(buffer.data()), buffer.length(), crc2); - REQUIRE(crc2 == 0x9498b398); -} diff --git a/tests/testUHID.cpp b/tests/testUHID.cpp new file mode 100644 index 0000000..163230c --- /dev/null +++ b/tests/testUHID.cpp @@ -0,0 +1,470 @@ +#include "catch2/catch_all.hpp" +#include +#include +#include +#include +#include +#include +#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; + +static void flush_sdl_events() { + SDL_JoystickUpdate(); + SDL_Event event; + while (SDL_PollEvent(&event) != 0) { + switch (event.type) { + case SDL_CONTROLLERDEVICEADDED: + std::cout << "SDL_CONTROLLERDEVICEADDED " << SDL_GameControllerNameForIndex(event.cdevice.which) << std::endl; + break; + case SDL_CONTROLLERDEVICEREMOVED: + std::cout << "SDL_CONTROLLERDEVICEREMOVED " << event.cdevice.which << std::endl; + break; + case SDL_CONTROLLERDEVICEREMAPPED: + std::cout << "SDL_CONTROLLERDEVICEREMAPPED " << SDL_GameControllerNameForIndex(event.cdevice.which) << std::endl; + break; + case SDL_CONTROLLERBUTTONDOWN: + case SDL_CONTROLLERBUTTONUP: + case SDL_JOYBUTTONDOWN: + case SDL_JOYBUTTONUP: + std::cout << "SDL button - " << (event.cbutton.state == SDL_PRESSED ? "pressed " : "released ") + << (int)event.cbutton.button << std::endl; + break; + case SDL_JOYAXISMOTION: + case SDL_CONTROLLERAXISMOTION: + std::cout << "SDL axis - " << (int)event.jaxis.axis << " " << event.jaxis.value << std::endl; + break; + case SDL_JOYHATMOTION: + std::cout << "SDL_JOYHATMOTION " << (int)event.jhat.value << std::endl; + break; + default: + std::cout << "SDL event: " << event.type << "\n"; + break; + } + } +} + +class SDLTestsFixture { +public: + SDLTestsFixture() { + if (SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC | SDL_INIT_GAMECONTROLLER | SDL_INIT_SENSOR | SDL_INIT_EVENTS) < + 0) { + std::cerr << "SDL could not initialize! SDL_Error: " << SDL_GetError() << std::endl; + } + SDL_LogSetAllPriority(SDL_LOG_PRIORITY_VERBOSE); + SDL_GameControllerEventState(SDL_ENABLE); + } + + ~SDLTestsFixture() { + SDL_Quit(); + } +}; + + +#define SDL_TEST_BUTTON(JOYPAD_BTN, SDL_BTN) \ + REQUIRE(SDL_GameControllerGetButton(gc, SDL_BTN) == 0); \ + joypad.set_pressed_buttons(JOYPAD_BTN); \ + flush_sdl_events(); \ + REQUIRE(SDL_GameControllerGetButton(gc, SDL_BTN) == 1); + +static void test_buttons(SDL_GameController *gc, Joypad &joypad) { + SDL_TEST_BUTTON(Joypad::DPAD_UP, SDL_CONTROLLER_BUTTON_DPAD_UP) + SDL_TEST_BUTTON(Joypad::DPAD_DOWN, SDL_CONTROLLER_BUTTON_DPAD_DOWN) + SDL_TEST_BUTTON(Joypad::DPAD_LEFT, SDL_CONTROLLER_BUTTON_DPAD_LEFT) + SDL_TEST_BUTTON(Joypad::DPAD_RIGHT, SDL_CONTROLLER_BUTTON_DPAD_RIGHT) + + SDL_TEST_BUTTON(Joypad::LEFT_STICK, SDL_CONTROLLER_BUTTON_LEFTSTICK) + SDL_TEST_BUTTON(Joypad::RIGHT_STICK, SDL_CONTROLLER_BUTTON_RIGHTSTICK) + SDL_TEST_BUTTON(Joypad::LEFT_BUTTON, SDL_CONTROLLER_BUTTON_LEFTSHOULDER) + SDL_TEST_BUTTON(Joypad::RIGHT_BUTTON, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) + + SDL_TEST_BUTTON(Joypad::A, SDL_CONTROLLER_BUTTON_A) + SDL_TEST_BUTTON(Joypad::B, SDL_CONTROLLER_BUTTON_B) + SDL_TEST_BUTTON(Joypad::X, SDL_CONTROLLER_BUTTON_X) + SDL_TEST_BUTTON(Joypad::Y, SDL_CONTROLLER_BUTTON_Y) + + SDL_TEST_BUTTON(Joypad::START, SDL_CONTROLLER_BUTTON_START) + SDL_TEST_BUTTON(Joypad::BACK, SDL_CONTROLLER_BUTTON_BACK) + SDL_TEST_BUTTON(Joypad::HOME, SDL_CONTROLLER_BUTTON_GUIDE) + + // Release all buttons + joypad.set_pressed_buttons(0); + flush_sdl_events(); + REQUIRE(SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_A) == 0); + REQUIRE(SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_B) == 0); + REQUIRE(SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_X) == 0); + REQUIRE(SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_Y) == 0); + + // Press some of them together + joypad.set_pressed_buttons(Joypad::A | Joypad::B | Joypad::X | Joypad::Y); + flush_sdl_events(); + REQUIRE(SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_A) == 1); + REQUIRE(SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_B) == 1); + REQUIRE(SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_X) == 1); + REQUIRE(SDL_GameControllerGetButton(gc, SDL_CONTROLLER_BUTTON_Y) == 1); +} + +/** + * @param power_supply_path (ex: + * "/sys/devices/virtual/misc/uhid/0003:054C:0CE6.0016/power_supply/ps-controller-battery-00:21:c1:75:88:38/") + * @return a pair of as read from the system + */ +std::pair get_system_battery(const std::filesystem::path &power_supply_path) { + // It's fairly simple, we have to read two files: capacity and status + std::ifstream capacity_file(power_supply_path / "capacity"); + std::ifstream status_file(power_supply_path / "status"); + if (!capacity_file.is_open() || !status_file.is_open()) { + return {0, "Unknown"}; + } + int capacity = 0; + std::string status; + capacity_file >> capacity; + status_file >> status; + return {capacity, status}; +} + +TEST_CASE_METHOD(SDLTestsFixture, "PS Joypad", "[SDL],[PS]") { + // Create the controller + auto joypad = std::move(*PS5Joypad::create()); + + std::this_thread::sleep_for(50ms); + + 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"); + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1"); + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_PLAYER_LED, "1"); + // Initializing the controller + flush_sdl_events(); + SDL_GameController *gc = SDL_GameControllerOpen(0); + if (gc == nullptr) { + WARN(SDL_GetError()); + } + REQUIRE(gc); + + REQUIRE(SDL_GameControllerGetType(gc) == SDL_CONTROLLER_TYPE_PS5); + { // Rumble + // Checking for basic capability + REQUIRE(SDL_GameControllerHasRumble(gc)); + + auto rumble_data = std::make_shared>(0, 0); + joypad.set_on_rumble([rumble_data](int low_freq, int high_freq) { + if (rumble_data->first == 0) + rumble_data->first = low_freq; + if (rumble_data->second == 0) + rumble_data->second = high_freq; + }); + + // When debugging this, bear in mind that SDL will send max duration here + // https://github.com/libsdl-org/SDL/blob/da8fc70a83cf6b76d5ea75c39928a7961bd163d3/src/joystick/linux/SDL_sysjoystick.c#L1628 + SDL_GameControllerRumble(gc, 0xFF00, 0xF00F, 100); + std::this_thread::sleep_for(30ms); // wait for the effect to be picked up + REQUIRE(rumble_data->first == 0x7f7f); + REQUIRE(rumble_data->second == 0x7878); + } + + test_buttons(gc, joypad); + { // Sticks + REQUIRE(SDL_GameControllerHasAxis(gc, SDL_CONTROLLER_AXIS_LEFTX)); + REQUIRE(SDL_GameControllerHasAxis(gc, SDL_CONTROLLER_AXIS_LEFTY)); + REQUIRE(SDL_GameControllerHasAxis(gc, SDL_CONTROLLER_AXIS_RIGHTX)); + REQUIRE(SDL_GameControllerHasAxis(gc, SDL_CONTROLLER_AXIS_RIGHTY)); + REQUIRE(SDL_GameControllerHasAxis(gc, SDL_CONTROLLER_AXIS_TRIGGERLEFT)); + REQUIRE(SDL_GameControllerHasAxis(gc, SDL_CONTROLLER_AXIS_TRIGGERRIGHT)); + + joypad.set_stick(Joypad::LS, 1000, 2000); + flush_sdl_events(); + REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTX) == 899); + REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTY) == -1928); + + joypad.set_stick(Joypad::RS, 1000, 2000); + flush_sdl_events(); + REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTX) == 899); + REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTY) == -1928); + + joypad.set_stick(Joypad::RS, -16384, -32768); + flush_sdl_events(); + REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTX) == -16320); + REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTY) == 32767); + + joypad.set_triggers(125, 255); + flush_sdl_events(); + REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_TRIGGERLEFT) == 16062); + REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_TRIGGERRIGHT) == 32767); + + joypad.set_triggers(0, 0); + flush_sdl_events(); + REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_TRIGGERLEFT) == 0); + REQUIRE(SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_TRIGGERRIGHT) == 0); + } + { // test acceleration + REQUIRE(SDL_GameControllerHasSensor(gc, SDL_SENSOR_ACCEL)); + if (SDL_GameControllerSetSensorEnabled(gc, SDL_SENSOR_ACCEL, SDL_TRUE) != 0) { + WARN(SDL_GetError()); + } + + std::array acceleration_data = {9.8f, 0.0f, 20.0f}; + joypad.set_motion(inputtino::PS5Joypad::ACCELERATION, + acceleration_data[0], + acceleration_data[1], + acceleration_data[2]); + SDL_GameControllerUpdate(); + SDL_SensorUpdate(); + SDL_Event event; + while (SDL_PollEvent(&event) != 0) { + if (event.type == SDL_CONTROLLERSENSORUPDATE) { + break; + } + } + REQUIRE(event.type == SDL_CONTROLLERSENSORUPDATE); + REQUIRE(event.csensor.sensor == SDL_SENSOR_ACCEL); + REQUIRE_THAT(event.csensor.data[0], WithinAbs(acceleration_data[0], 0.9f)); + REQUIRE_THAT(event.csensor.data[1], WithinAbs(acceleration_data[1], 0.9f)); + REQUIRE_THAT(event.csensor.data[2], WithinAbs(acceleration_data[2], 0.9f)); + flush_sdl_events(); + + // Now lets test the negatives + acceleration_data = {-9.8f, -0.0f, -20.0f}; + joypad.set_motion(inputtino::PS5Joypad::ACCELERATION, + acceleration_data[0], + acceleration_data[1], + acceleration_data[2]); + SDL_GameControllerUpdate(); + SDL_SensorUpdate(); + while (SDL_PollEvent(&event) != 0) { + if (event.type == SDL_CONTROLLERSENSORUPDATE) { + break; + } + } + REQUIRE(event.type == SDL_CONTROLLERSENSORUPDATE); + REQUIRE(event.csensor.sensor == SDL_SENSOR_ACCEL); + REQUIRE_THAT(event.csensor.data[0], WithinAbs(acceleration_data[0], 0.9f)); + REQUIRE_THAT(event.csensor.data[1], WithinAbs(acceleration_data[1], 0.9f)); + REQUIRE_THAT(event.csensor.data[2], WithinAbs(acceleration_data[2], 0.9f)); + flush_sdl_events(); + } + { // test gyro + REQUIRE(SDL_GameControllerHasSensor(gc, SDL_SENSOR_GYRO)); + if (SDL_GameControllerSetSensorEnabled(gc, SDL_SENSOR_GYRO, SDL_TRUE) != 0) { + WARN(SDL_GetError()); + } + + std::array gyro_data = {0.0f, + M_PI_2f, // half a turn (180 degrees) + M_PIf}; // full turn (360 degrees) + joypad.set_motion(inputtino::PS5Joypad::GYROSCOPE, gyro_data[0], gyro_data[1], gyro_data[2]); + std::this_thread::sleep_for(10ms); + + SDL_GameControllerUpdate(); + SDL_SensorUpdate(); + SDL_Event event; + while (SDL_PollEvent(&event) != 0) { + if (event.type == SDL_CONTROLLERSENSORUPDATE) { + break; + } + } + REQUIRE(event.type == SDL_CONTROLLERSENSORUPDATE); + REQUIRE(event.csensor.sensor == SDL_SENSOR_GYRO); + REQUIRE_THAT(event.csensor.data[0], WithinAbs(gyro_data[0], 0.01f)); + REQUIRE_THAT(event.csensor.data[1], WithinAbs(gyro_data[1], 0.01f)); + REQUIRE_THAT(event.csensor.data[2], WithinAbs(gyro_data[2], 0.01f)); + flush_sdl_events(); + + // Now let's try the negatives + gyro_data = { + -0.0f, + -M_PI_2f, // half a turn (180 degrees) + -M_PIf // full turn (360 degrees) + }; + joypad.set_motion(inputtino::PS5Joypad::GYROSCOPE, gyro_data[0], gyro_data[1], gyro_data[2]); + std::this_thread::sleep_for(10ms); + + SDL_GameControllerUpdate(); + SDL_SensorUpdate(); + while (SDL_PollEvent(&event) != 0) { + if (event.type == SDL_CONTROLLERSENSORUPDATE) { + break; + } + } + REQUIRE(event.type == SDL_CONTROLLERSENSORUPDATE); + REQUIRE(event.csensor.sensor == SDL_SENSOR_GYRO); + REQUIRE_THAT(event.csensor.data[0], WithinAbs(gyro_data[0], 0.01f)); + REQUIRE_THAT(event.csensor.data[1], WithinAbs(gyro_data[1], 0.01f)); + REQUIRE_THAT(event.csensor.data[2], WithinAbs(gyro_data[2], 0.01f)); + flush_sdl_events(); + + // Try out problematic values from https://github.com/LizardByte/Sunshine/issues/3247 + gyro_data = {-32769.0f, 32769.0f, -0.0004124999977648258f}; + joypad.set_motion(inputtino::PS5Joypad::GYROSCOPE, gyro_data[0], gyro_data[1], gyro_data[2]); + std::this_thread::sleep_for(10ms); + + SDL_GameControllerUpdate(); + SDL_SensorUpdate(); + while (SDL_PollEvent(&event) != 0) { + if (event.type == SDL_CONTROLLERSENSORUPDATE) { + break; + } + } + REQUIRE(event.type == SDL_CONTROLLERSENSORUPDATE); + REQUIRE(event.csensor.sensor == SDL_SENSOR_GYRO); + REQUIRE_THAT(event.csensor.data[0], WithinAbs(-28.59546f, 0.01f)); + REQUIRE_THAT(event.csensor.data[1], WithinAbs(28.59546f, 0.01f)); + REQUIRE_THAT(event.csensor.data[2], WithinAbs(0.0f, 0.01f)); + flush_sdl_events(); + } + + { // LED TODO: seems that this only works after some gyro/acceleration data is sent + REQUIRE(SDL_GameControllerHasLED(gc)); + struct LED { + int r; + int g; + int b; + }; + auto led_data = std::make_shared(); + joypad.set_on_led([led_data](int r, int g, int b) { + led_data->r = r; + led_data->g = g; + led_data->b = b; + }); + REQUIRE(SDL_GameControllerSetLED(gc, 50, 100, 150) == 0); + std::this_thread::sleep_for(20ms); // wait for the effect to be picked up + REQUIRE(led_data->r == 50); + REQUIRE(led_data->g == 100); + REQUIRE(led_data->b == 150); + } + + { // Test touchpad + REQUIRE(SDL_GameControllerGetNumTouchpads(gc) == 1); + REQUIRE(SDL_GameControllerGetNumTouchpadFingers(gc, 0) == 2); + + // TODO: test these values with SDL + joypad.place_finger(0, 1920, 1080); + joypad.place_finger(1, 1920, 1080); + joypad.release_finger(0); + joypad.release_finger(1); + } + + { // Test battery + auto joy = SDL_GameControllerGetJoystick(gc); + REQUIRE(SDL_JoystickCurrentPowerLevel(joy) == SDL_JOYSTICK_POWER_FULL); + + auto base_path = + std::filesystem::path( + joypad.get_sys_nodes()[0]) // "/sys/devices/virtual/misc/uhid/0003:054C:0CE6.0017/input/input123" + .parent_path() // "/sys/devices/virtual/misc/uhid/0003:054C:0CE6.0017/input/" + .parent_path() // "/sys/devices/virtual/misc/uhid/0003:054C:0CE6.0017/" + / "power_supply" // "/sys/devices/virtual/misc/uhid/0003:054C:0CE6.0017/power_supply" + / ("ps-controller-battery-" + joypad.get_mac_address()); + REQUIRE(std::filesystem::exists(base_path)); + + { // Defaults to full if nothing is set + auto [capacity, status] = get_system_battery(base_path); + REQUIRE(capacity == 100); + REQUIRE(status == "Full"); + } + + { + joypad.set_battery(inputtino::PS5Joypad::BATTERY_CHARGHING, 80); + auto [capacity, status] = get_system_battery(base_path); + REQUIRE(capacity == 85); + REQUIRE(status == "Charging"); + } + + { + joypad.set_battery(inputtino::PS5Joypad::BATTERY_CHARGHING, 10); + auto [capacity, status] = get_system_battery(base_path); + REQUIRE(capacity == 15); + REQUIRE(status == "Charging"); + } + + { + joypad.set_battery(inputtino::PS5Joypad::BATTERY_DISCHARGING, 75); + auto [capacity, status] = get_system_battery(base_path); + REQUIRE(capacity == 75); + REQUIRE(status == "Discharging"); + } + + { + joypad.set_battery(inputtino::PS5Joypad::BATTERY_FULL, 100); + auto [capacity, status] = get_system_battery(base_path); + REQUIRE(capacity == 100); + REQUIRE(status == "Full"); + } + } + + // Adaptive triggers aren't directly supported by SDL + // see:https://github.com/libsdl-org/SDL/issues/5125#issuecomment-1204261666 + // see: HIDAPI_DriverPS5_RumbleJoystickTriggers() + // but we can send custom data to the device, the following code is adapted from + // https://github.com/libsdl-org/SDL/blob/d66483dfccfcdc4e03f719e318c7a76f963f22d9/test/testcontroller.c#L235-L255 + { + auto trigger_event = std::make_shared(); + joypad.set_on_trigger_effect([trigger_event](const PS5Joypad::TriggerEffect &effect) { *trigger_event = effect; }); + + /* Resistance and vibration when trigger is pulled */ + uint8_t left_effect_type = 0x06; + uint8_t left_effect[10] = {15, 63, 128, 0, 0, 0, 0, 0, 0, 0}; + /* Constant resistance across entire trigger pull */ + uint8_t right_effect_type = 0x01; + uint8_t right_effect[10] = {0, 110, 0, 0, 0, 0, 0, 0, 0, 0}; + + uhid::dualsense_output_report_common state = {}; + SDL_zero(state); + state.valid_flag0 |= (uhid::RIGHT_TRIGGER_EFFECT); + state.right_trigger_effect_type = right_effect_type; + SDL_memcpy(state.right_trigger_effect, right_effect, sizeof(right_effect)); + state.left_trigger_effect_type = left_effect_type; + SDL_memcpy(state.left_trigger_effect, left_effect, sizeof(left_effect)); + SDL_GameControllerSendEffect(gc, &state, sizeof(state)); + + std::this_thread::sleep_for(15ms); + flush_sdl_events(); + REQUIRE(trigger_event->event_flags == uhid::RIGHT_TRIGGER_EFFECT); + REQUIRE(trigger_event->type_left == left_effect_type); + REQUIRE(trigger_event->type_right == right_effect_type); + REQUIRE(std::equal(std::begin(trigger_event->left), std::end(trigger_event->left), std::begin(left_effect))); + REQUIRE(std::equal(std::begin(trigger_event->right), std::end(trigger_event->right), std::begin(right_effect))); + } + + { // Test creating a second device + REQUIRE(SDL_NumJoysticks() == 1); + auto joypad2 = std::move(*PS5Joypad::create()); + std::this_thread::sleep_for(50ms); + + auto devices2 = joypad2.get_nodes(); + REQUIRE_THAT(devices2, SizeIs(5)); // 3 eventXX and 2 jsYY + REQUIRE_THAT(devices2, Contains(ContainsSubstring("/dev/input/event"))); + REQUIRE_THAT(devices2, Contains(ContainsSubstring("/dev/input/js"))); + + flush_sdl_events(); + REQUIRE(SDL_NumJoysticks() == 2); + SDL_GameController *gc2 = SDL_GameControllerOpen(1); + REQUIRE(SDL_GameControllerGetType(gc2) == SDL_CONTROLLER_TYPE_PS5); + SDL_GameControllerClose(gc2); + } + + SDL_GameControllerClose(gc); +} + +TEST_CASE("Bluetooth CRC32", "[PS]") { + std::string buffer = "123456789"; + auto crc = CRC32(reinterpret_cast(buffer.data()), buffer.length()); + REQUIRE(crc == 0xcbf43926); // https://crccalc.com/?crc=123456789&method=CRC-32/ISO-HDLC&datatype=ascii&outtype=hex + + unsigned char PS_INPUT_CRC32_SEED = 0xA1; + auto crc2 = CRC32(&PS_INPUT_CRC32_SEED, 1, 0xFFFFFFFF); + crc2 = CRC32(reinterpret_cast(buffer.data()), buffer.length(), crc2); + REQUIRE(crc2 == 0x9498b398); +} \ No newline at end of file