From 3016c7f1b3b07e5c63cdc762b370eda3dc55ad0d Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 16 Dec 2025 13:42:18 -0600 Subject: [PATCH] feat(hid): Update `hid-rp` to have support for Dualshock 4 hid report. --- .../hid-rp/example/main/hid_rp_example.cpp | 31 + .../hid-rp/include/hid-rp-ps4-formatters.hpp | 56 ++ components/hid-rp/include/hid-rp-ps4.hpp | 598 ++++++++++++++++++ components/hid_service/example/README.md | 30 +- .../example/main/Kconfig.projbuild | 4 + .../example/main/hid_service_example.cpp | 140 +++- doc/Doxyfile | 1 + doc/en/hid/hid-rp.rst | 3 +- 8 files changed, 858 insertions(+), 5 deletions(-) create mode 100644 components/hid-rp/include/hid-rp-ps4-formatters.hpp create mode 100644 components/hid-rp/include/hid-rp-ps4.hpp diff --git a/components/hid-rp/example/main/hid_rp_example.cpp b/components/hid-rp/example/main/hid_rp_example.cpp index d87a34d74..05ae22c6b 100644 --- a/components/hid-rp/example/main/hid_rp_example.cpp +++ b/components/hid-rp/example/main/hid_rp_example.cpp @@ -5,6 +5,7 @@ #include "hid-rp-gamepad.hpp" #include "hid-rp-playstation.hpp" +#include "hid-rp-ps4.hpp" #include "hid-rp-switch-pro.hpp" #include "hid-rp-xbox.hpp" #include "hid-rp.hpp" @@ -90,6 +91,23 @@ extern "C" void app_main(void) { } logger.info(" Data: [{}]", str); + using PS4DualShock4Input = espp::PS4DualShock4GamepadInputReport<>; + PS4DualShock4Input ps4_input_report; + logger.info("{}", ps4_input_report); + logger.info("PS4 DualShock 4 Input Report Size: {}", ps4_input_report.get_report().size()); + logger.info("PS4 DualShock 4 Input Report Data: {::#04X}", ps4_input_report.get_report()); + + auto ps4_raw_descriptor = espp::ps4_dualshock4_descriptor(); + auto ps4_descriptor = std::vector(ps4_raw_descriptor.begin(), ps4_raw_descriptor.end()); + + logger.info("PS4 DualShock 4 Report Descriptor:"); + logger.info(" Size: {}", ps4_descriptor.size()); + str = ""; + for (auto &byte : ps4_descriptor) { + str += fmt::format("0x{:02X}, ", byte); + } + logger.info(" Data: [{}]", str); + GamepadInput::Hat hat = GamepadInput::Hat::UP_RIGHT; int button_index = 5; float angle = 2.0f * M_PI * button_index / num_buttons; @@ -100,6 +118,7 @@ extern "C" void app_main(void) { switch_pro_input_report.reset(); dualsense_simple_input_report.reset(); dualsense_complex_input_report.reset(); + ps4_input_report.reset(); // print out the reports in their default states logger.info("{}", gamepad_input_report); @@ -107,6 +126,7 @@ extern "C" void app_main(void) { logger.info("{}", switch_pro_input_report); logger.info("{}", dualsense_simple_input_report); logger.info("{}", dualsense_complex_input_report); + logger.info("{}", ps4_input_report); // update the gamepad input report logger.info("{}", gamepad_input_report); @@ -140,6 +160,17 @@ extern "C" void app_main(void) { dualsense_complex_input_report.set_left_trigger(std::abs(cos(angle))); dualsense_complex_input_report.set_right_trigger(std::abs(sin(angle))); + ps4_input_report.set_hat(hat); + ps4_input_report.set_button_cross(button_index == 1); + ps4_input_report.set_button_circle(button_index == 2); + ps4_input_report.set_button_square(button_index == 3); + ps4_input_report.set_button_triangle(button_index == 4); + ps4_input_report.set_left_joystick(128 + 127 * sin(angle), 128 + 127 * cos(angle)); + ps4_input_report.set_right_joystick(128 + 127 * cos(angle), 128 + 127 * sin(angle)); + ps4_input_report.set_l2_trigger(std::abs(cos(angle)) * 255); + ps4_input_report.set_r2_trigger(std::abs(sin(angle)) * 255); + ps4_input_report.set_battery_level(8); + button_index = (button_index % num_buttons) + 1; // send an input report diff --git a/components/hid-rp/include/hid-rp-ps4-formatters.hpp b/components/hid-rp/include/hid-rp-ps4-formatters.hpp new file mode 100644 index 000000000..63ab5ddfa --- /dev/null +++ b/components/hid-rp/include/hid-rp-ps4-formatters.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include "format.hpp" + +template +struct fmt::formatter> { + template constexpr auto parse(ParseContext &ctx) const { + return ctx.begin(); + } + + template + auto format(const espp::PS4DualShock4GamepadInputReport &report, + FormatContext &ctx) const { + auto out = ctx.out(); + fmt::format_to(out, "PS4DualShock4GamepadInputReport {{"); + fmt::format_to(out, "left_stick: ({}, {}), ", report.get_left_joystick_x(), + report.get_left_joystick_y()); + fmt::format_to(out, "right_stick: ({}, {}), ", report.get_right_joystick_x(), + report.get_right_joystick_y()); + fmt::format_to(out, "triggers: (L2={}, R2={}), ", report.get_l2_trigger(), + report.get_r2_trigger()); + fmt::format_to(out, "hat: {}, ", static_cast(report.get_hat())); + fmt::format_to(out, "buttons: ["); + fmt::format_to(out, "Square={}, Cross={}, Circle={}, Triangle={}, ", report.get_button_square(), + report.get_button_cross(), report.get_button_circle(), + report.get_button_triangle()); + fmt::format_to(out, "L1={}, R1={}, L2={}, R2={}, ", report.get_button_l1(), + report.get_button_r1(), report.get_button_l2(), report.get_button_r2()); + fmt::format_to(out, "Share={}, Options={}, L3={}, R3={}, ", report.get_button_share(), + report.get_button_options(), report.get_button_l3(), report.get_button_r3()); + fmt::format_to(out, "PS={}, Touchpad={}], ", report.get_button_home(), + report.get_button_touchpad()); + fmt::format_to(out, "battery: {}%, charging: {}, ", report.get_battery_level(), + report.get_battery_charging()); + auto gyro = report.get_gyroscope(); + auto accel = report.get_accelerometer(); + fmt::format_to(out, "gyro: ({}, {}, {}), ", gyro.X, gyro.Y, gyro.Z); + fmt::format_to(out, "accel: ({}, {}, {}), ", accel.X, accel.Y, accel.Z); + fmt::format_to(out, "timestamp: {}, counter: {}", report.get_timestamp(), report.get_counter()); + return fmt::format_to(out, "}}"); + } +}; + +template struct fmt::formatter> { + template constexpr auto parse(ParseContext &ctx) const { + return ctx.begin(); + } + + template + auto format(const espp::PS4DualShock4OutputReport &report, FormatContext &ctx) const { + auto out = ctx.out(); + fmt::format_to(out, "PS4DualShock4OutputReport {{"); + fmt::format_to(out, "data: [{::#02X}]", report.data); + return fmt::format_to(out, "}}"); + } +}; diff --git a/components/hid-rp/include/hid-rp-ps4.hpp b/components/hid-rp/include/hid-rp-ps4.hpp new file mode 100644 index 000000000..939e52a7f --- /dev/null +++ b/components/hid-rp/include/hid-rp-ps4.hpp @@ -0,0 +1,598 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "gamepad_imu.hpp" +#include "hid-rp-gamepad.hpp" + +namespace espp { + +#pragma pack(push, 1) + +/// PS4 DualShock 4 Gamepad Buttons +/// Used in input report 0x01 over BLE +union PS4DualShock4GamepadButtons { + std::array raw; + struct { + // byte 0 + std::uint8_t hat_switch : 4; ///< Hat switch (d-pad) value + std::uint8_t square : 1; ///< Square button + std::uint8_t cross : 1; ///< Cross button + std::uint8_t circle : 1; ///< Circle button + std::uint8_t triangle : 1; ///< Triangle button + // byte 1 + std::uint8_t l1 : 1; ///< L1 button + std::uint8_t r1 : 1; ///< R1 button + std::uint8_t l2 : 1; ///< L2 button + std::uint8_t r2 : 1; ///< R2 button + std::uint8_t share : 1; ///< Share button + std::uint8_t options : 1; ///< Options button + std::uint8_t l3 : 1; ///< L3 button + std::uint8_t r3 : 1; ///< R3 button + // byte 2 + std::uint8_t home : 1; ///< PS button + std::uint8_t touchpad : 1; ///< Touchpad button + std::uint8_t counter : 6; ///< Report counter + }; +}; + +/// PS4 DualShock 4 Touchpad Data +struct PS4TouchpadData { + std::uint8_t touch_id : 7; ///< Touch ID + std::uint8_t active : 1; ///< Active touch flag + std::uint16_t x : 12; ///< X position (0-1919) + std::uint16_t y : 12; ///< Y position (0-941) + std::uint8_t padding : 4; ///< Padding +}; + +/// PS4 DualShock 4 Touchpad Report +struct PS4TouchpadReport { + std::uint8_t timestamp; ///< Timestamp + PS4TouchpadData touch[2]; ///< Two touch points +}; + +#pragma pack(pop) + +/// HID PS4 DualShock 4 Gamepad Input Report +/// +/// This class implements a HID PS4 DualShock 4 Gamepad Input Report for BLE. +/// It supports 14 buttons, a d-pad (hat switch), 2 joystick axes, 2 analog triggers, +/// touchpad data, IMU (accelerometer and gyroscope), and battery status. +/// +/// The PS4 DualShock 4 uses report ID 0x01 for input reports over BLE (77 bytes). +/// The report structure follows the official Sony PS4 DualShock 4 HID descriptor. +/// +/// Button mapping: +/// - Buttons 1-4: Square, Cross, Circle, Triangle +/// - Buttons 5-8: L1, R1, L2, R2 +/// - Buttons 9-12: Share, Options, L3, R3 +/// - Buttons 13-14: PS Home, Touchpad Click +/// +/// \section hid_rp_ps4_references References +/// - Protocol documentation: https://www.psdevwiki.com/ps4/DS4-BT +/// - Protocol documentation: https://www.psdevwiki.com/ps4/DS4-USB +/// - HID descriptor: +/// https://github.com/DJm00n/ControllersInfo/blob/master/dualshock4/dualshock4_hid_report_descriptor.txt +/// - Linux kernel: https://github.com/torvalds/linux/blob/master/drivers/hid/hid-sony.c +/// - Community reverse engineering: https://github.com/chrippa/ds4drv +/// +/// \section hid_rp_ps4_ex1 HID-RP PS4 Example +/// \snippet hid_rp_example.cpp hid rp example +template +class PS4DualShock4GamepadInputReport + : public hid::report::base { +public: + using Hat = espp::gamepad::Hat; + using Accelerometer = espp::gamepad::Accelerometer; + using Gyroscope = espp::gamepad::Gyroscope; + + static constexpr size_t button_count = 14; + + using JOYSTICK_TYPE = std::uint8_t; + static constexpr JOYSTICK_TYPE joystick_min = 0; + static constexpr JOYSTICK_TYPE joystick_max = 255; + static constexpr JOYSTICK_TYPE joystick_center = 128; + static constexpr size_t joystick_value_range = joystick_max - joystick_min; + static constexpr JOYSTICK_TYPE joystick_range = joystick_value_range / 2; + + using TRIGGER_TYPE = std::uint8_t; + static constexpr TRIGGER_TYPE trigger_min = 0; + static constexpr TRIGGER_TYPE trigger_max = 255; + +protected: +#pragma pack(push, 1) + union { + struct { + std::uint8_t left_stick_x; ///< Left stick X axis + std::uint8_t left_stick_y; ///< Left stick Y axis + std::uint8_t right_stick_x; ///< Right stick X axis + std::uint8_t right_stick_y; ///< Right stick Y axis + PS4DualShock4GamepadButtons buttons; ///< Button states + std::uint8_t l2_trigger; ///< L2 trigger analog value + std::uint8_t r2_trigger; ///< R2 trigger analog value + std::uint16_t timestamp; ///< Timestamp + std::uint8_t battery; ///< Battery level + std::int16_t gyro_x; ///< Gyroscope X axis + std::int16_t gyro_y; ///< Gyroscope Y axis + std::int16_t gyro_z; ///< Gyroscope Z axis + std::int16_t accel_x; ///< Accelerometer X axis + std::int16_t accel_y; ///< Accelerometer Y axis + std::int16_t accel_z; ///< Accelerometer Z axis + std::uint8_t reserved[5]; ///< Reserved bytes + std::uint8_t extension; ///< Extension byte + std::uint8_t reserved2[2]; ///< Reserved bytes + PS4TouchpadReport touchpad[3]; ///< Up to 3 touchpad reports + std::uint8_t reserved3[3]; ///< Reserved bytes + }; + std::array raw; ///< Raw report data + }; +#pragma pack(pop) + +public: + static constexpr size_t num_data_bytes = sizeof(raw); + constexpr PS4DualShock4GamepadInputReport() { reset(); } + + constexpr void reset() { + std::fill(raw.begin(), raw.end(), 0); + left_stick_x = joystick_center; + left_stick_y = joystick_center; + right_stick_x = joystick_center; + right_stick_y = joystick_center; + buttons.raw[0] = 0x08; // hat_switch centered + } + + // Joystick methods + constexpr void set_left_joystick(JOYSTICK_TYPE x, JOYSTICK_TYPE y) { + left_stick_x = x; + left_stick_y = y; + } + + constexpr void set_right_joystick(JOYSTICK_TYPE x, JOYSTICK_TYPE y) { + right_stick_x = x; + right_stick_y = y; + } + + constexpr void set_left_joystick_x(JOYSTICK_TYPE x) { left_stick_x = x; } + constexpr void set_left_joystick_y(JOYSTICK_TYPE y) { left_stick_y = y; } + constexpr void set_right_joystick_x(JOYSTICK_TYPE x) { right_stick_x = x; } + constexpr void set_right_joystick_y(JOYSTICK_TYPE y) { right_stick_y = y; } + + constexpr JOYSTICK_TYPE get_left_joystick_x() const { return left_stick_x; } + constexpr JOYSTICK_TYPE get_left_joystick_y() const { return left_stick_y; } + constexpr JOYSTICK_TYPE get_right_joystick_x() const { return right_stick_x; } + constexpr JOYSTICK_TYPE get_right_joystick_y() const { return right_stick_y; } + + // Trigger methods + constexpr void set_l2_trigger(TRIGGER_TYPE value) { l2_trigger = value; } + constexpr void set_r2_trigger(TRIGGER_TYPE value) { r2_trigger = value; } + constexpr TRIGGER_TYPE get_l2_trigger() const { return l2_trigger; } + constexpr TRIGGER_TYPE get_r2_trigger() const { return r2_trigger; } + + // D-pad / Hat methods + constexpr void set_hat(Hat hat_value) { + buttons.hat_switch = static_cast(hat_value); + } + + constexpr Hat get_hat() const { return static_cast(buttons.hat_switch); } + + // Button methods + constexpr void set_button_square(bool pressed) { buttons.square = pressed; } + constexpr void set_button_cross(bool pressed) { buttons.cross = pressed; } + constexpr void set_button_circle(bool pressed) { buttons.circle = pressed; } + constexpr void set_button_triangle(bool pressed) { buttons.triangle = pressed; } + constexpr void set_button_l1(bool pressed) { buttons.l1 = pressed; } + constexpr void set_button_r1(bool pressed) { buttons.r1 = pressed; } + constexpr void set_button_l2(bool pressed) { buttons.l2 = pressed; } + constexpr void set_button_r2(bool pressed) { buttons.r2 = pressed; } + constexpr void set_button_share(bool pressed) { buttons.share = pressed; } + constexpr void set_button_options(bool pressed) { buttons.options = pressed; } + constexpr void set_button_l3(bool pressed) { buttons.l3 = pressed; } + constexpr void set_button_r3(bool pressed) { buttons.r3 = pressed; } + constexpr void set_button_home(bool pressed) { buttons.home = pressed; } + constexpr void set_button_touchpad(bool pressed) { buttons.touchpad = pressed; } + + constexpr bool get_button_square() const { return buttons.square; } + constexpr bool get_button_cross() const { return buttons.cross; } + constexpr bool get_button_circle() const { return buttons.circle; } + constexpr bool get_button_triangle() const { return buttons.triangle; } + constexpr bool get_button_l1() const { return buttons.l1; } + constexpr bool get_button_r1() const { return buttons.r1; } + constexpr bool get_button_l2() const { return buttons.l2; } + constexpr bool get_button_r2() const { return buttons.r2; } + constexpr bool get_button_share() const { return buttons.share; } + constexpr bool get_button_options() const { return buttons.options; } + constexpr bool get_button_l3() const { return buttons.l3; } + constexpr bool get_button_r3() const { return buttons.r3; } + constexpr bool get_button_home() const { return buttons.home; } + constexpr bool get_button_touchpad() const { return buttons.touchpad; } + + // Battery methods + constexpr void set_battery_level(std::uint8_t level) { + battery = (level & 0x0F) | (battery & 0xF0); + } + + constexpr void set_battery_charging(bool charging) { + if (charging) { + battery |= 0x10; + } else { + battery &= 0x0F; + } + } + + constexpr std::uint8_t get_battery_level() const { return battery & 0x0F; } + constexpr bool get_battery_charging() const { return (battery & 0x10) != 0; } + + // IMU methods + constexpr void set_gyroscope(const Gyroscope &gyro) { + gyro_x = gyro.X; + gyro_y = gyro.Y; + gyro_z = gyro.Z; + } + + constexpr void set_accelerometer(const Accelerometer &accel) { + accel_x = accel.X; + accel_y = accel.Y; + accel_z = accel.Z; + } + + constexpr Gyroscope get_gyroscope() const { return {{gyro_x, gyro_y, gyro_z}}; } + + constexpr Accelerometer get_accelerometer() const { return {{accel_x, accel_y, accel_z}}; } + + // Timestamp methods + constexpr void set_timestamp(std::uint16_t ts) { timestamp = ts; } + constexpr std::uint16_t get_timestamp() const { return timestamp; } + + // Counter methods + constexpr void set_counter(std::uint8_t count) { buttons.counter = count & 0x3F; } + constexpr std::uint8_t get_counter() const { return buttons.counter; } + + // Touchpad methods + constexpr void set_touchpad_data(size_t report_idx, size_t touch_idx, bool active, + std::uint16_t x, std::uint16_t y) { + if (report_idx < 3 && touch_idx < 2) { + auto &touch = touchpad[report_idx].touch[touch_idx]; + touch.active = active; + touch.x = x & 0x0FFF; + touch.y = y & 0x0FFF; + } + } + + constexpr void set_touchpad_timestamp(size_t report_idx, std::uint8_t ts) { + if (report_idx < 3) { + touchpad[report_idx].timestamp = ts; + } + } + + // Serialization methods + constexpr auto get_report() const { return std::vector(raw.begin() + 1, raw.end()); } + + constexpr void set_data(const std::vector &data) { + std::copy(data.begin(), data.end(), raw.begin() + 1); + } + + static constexpr auto get_descriptor() { + using namespace hid::page; + using namespace hid::rdf; + + // clang-format off + // Based on official PS4 DualShock 4 Bluetooth HID descriptor + // Reference: https://github.com/DJm00n/ControllersInfo/blob/master/dualshock4/dualshock4_hid_report_descriptor.txt + // Reference: Linux kernel drivers/hid/hid-sony.c + return descriptor( + conditional_report_id(), + usage_page(), + + // Left stick X, Y and Right stick X (Z), Y (Rz) - 4 bytes + usage(generic_desktop::X), + usage(generic_desktop::Y), + usage(generic_desktop::Z), + usage(generic_desktop::RZ), + logical_limits<1, 1>(joystick_min, joystick_max), + report_size(8), + report_count(4), + input::absolute_variable(), + + // D-pad / Hat switch - 4 bits + usage(generic_desktop::HAT_SWITCH), + logical_limits<1, 1>(0, 7), + physical_limits<1, 2>(0, 315), + unit::unit<1>(unit::code::DEGREE), + report_size(4), + report_count(1), + input::absolute_variable(input::flags::NULL_STATE), + + unit::unit<1>(unit::code::NONE), + + // Face buttons: Square, Cross, Circle, Triangle - 4 bits + usage_page