From b8e56dc6d56b75447557f7f5b12066cf1ae46836 Mon Sep 17 00:00:00 2001 From: Joel Coelho Date: Thu, 20 Aug 2026 12:22:21 +0100 Subject: [PATCH 1/2] feat: expose DS5 player-indicator and mic-mute LED outputs Add player_led and mic_led to GamepadOutputKind and GamepadOutput, and parse them from the DualSense output report in append_dualsense_outputs (player indicator gated by valid_flag1 bit 0x10 at offset+43; mic-mute LED gated by bit 0x01 at offset+8), mirroring the existing rgb_led handling. The DS5 profile advertises both via new supports_player_led / supports_mic_led capability flags. Signed-off-by: Joel Coelho --- src/core/gamepad_adapter.cpp | 6 +++ src/core/profiles.cpp | 2 + src/core/report.cpp | 18 +++++++++ src/include/libvirtualhid/gamepad_adapter.hpp | 10 +++++ src/include/libvirtualhid/types.hpp | 22 +++++++++++ tests/unit/test_gamepad_adapter.cpp | 4 ++ tests/unit/test_profiles.cpp | 2 + tests/unit/test_report.cpp | 39 +++++++++++++++++++ tests/unit/test_virtualhid_control_model.cpp | 2 + tools/virtualhid_control_model.cpp | 4 ++ 10 files changed, 109 insertions(+) diff --git a/src/core/gamepad_adapter.cpp b/src/core/gamepad_adapter.cpp index d7360e5..6998048 100644 --- a/src/core/gamepad_adapter.cpp +++ b/src/core/gamepad_adapter.cpp @@ -83,6 +83,8 @@ namespace lvh { support.supports_rumble = profile.capabilities.supports_rumble; support.supports_rgb_led = profile.capabilities.supports_rgb_led; support.supports_adaptive_triggers = profile.capabilities.supports_adaptive_triggers; + support.supports_player_led = profile.capabilities.supports_player_led; + support.supports_mic_led = profile.capabilities.supports_mic_led; support.supports_motion = profile.capabilities.supports_motion; support.supports_touchpad = profile.capabilities.supports_touchpad; support.supports_battery = profile.capabilities.supports_battery; @@ -144,6 +146,10 @@ namespace lvh { return support.supports_adaptive_triggers; case raw_report: return profile.output_report_size > 0U; + case player_led: + return support.supports_player_led; + case mic_led: + return support.supports_mic_led; } return false; diff --git a/src/core/profiles.cpp b/src/core/profiles.cpp index 67f843f..2e99caf 100644 --- a/src/core/profiles.cpp +++ b/src/core/profiles.cpp @@ -2012,6 +2012,8 @@ namespace lvh::profiles { .supports_rgb_led = true, .supports_battery = true, .supports_adaptive_triggers = true, + .supports_player_led = true, + .supports_mic_led = true, }; profile.report_descriptor = bus_type == BusType::bluetooth ? make_dualsense_bluetooth_report_descriptor() : make_dualsense_usb_report_descriptor(); diff --git a/src/core/report.cpp b/src/core/report.cpp index d0fa18b..b11f070 100644 --- a/src/core/report.cpp +++ b/src/core/report.cpp @@ -61,7 +61,9 @@ namespace lvh::reports { constexpr auto dualsense_flag0_left_trigger = std::byte {0x08}; + constexpr auto dualsense_flag1_mic_mute_led = std::byte {0x01}; constexpr auto dualsense_flag1_lightbar = std::byte {0x04}; + constexpr auto dualsense_flag1_player_indicator = std::byte {0x10}; constexpr auto dualsense_flag2_compatible_vibration = std::byte {0x04}; @@ -936,6 +938,22 @@ namespace lvh::reports { outputs.push_back(std::move(output)); } + if (has_flag(valid_flag1, dualsense_flag1_player_indicator)) { + GamepadOutput output; + output.kind = GamepadOutputKind::player_led; + output.player_led = raw_report[offset + 43U]; + output.raw_report = raw_report; + outputs.push_back(std::move(output)); + } + + if (has_flag(valid_flag1, dualsense_flag1_mic_mute_led)) { + GamepadOutput output; + output.kind = GamepadOutputKind::mic_led; + output.mic_led = raw_report[offset + 8U]; + output.raw_report = raw_report; + outputs.push_back(std::move(output)); + } + const auto trigger_flags = valid_flag0 & (dualsense_flag0_left_trigger | dualsense_flag0_right_trigger); if (trigger_flags != zero_byte) { GamepadOutput output; diff --git a/src/include/libvirtualhid/gamepad_adapter.hpp b/src/include/libvirtualhid/gamepad_adapter.hpp index 1ce7344..5893f54 100644 --- a/src/include/libvirtualhid/gamepad_adapter.hpp +++ b/src/include/libvirtualhid/gamepad_adapter.hpp @@ -40,6 +40,16 @@ namespace lvh { */ bool supports_adaptive_triggers = false; + /** + * @brief Whether the profile supports player-indicator LED output. + */ + bool supports_player_led = false; + + /** + * @brief Whether the profile supports mic-mute LED output. + */ + bool supports_mic_led = false; + /** * @brief Whether the profile exposes motion sensor input. */ diff --git a/src/include/libvirtualhid/types.hpp b/src/include/libvirtualhid/types.hpp index 260fca5..4f4f050 100644 --- a/src/include/libvirtualhid/types.hpp +++ b/src/include/libvirtualhid/types.hpp @@ -270,6 +270,16 @@ namespace lvh { * @brief Whether the profile supports adaptive trigger output. */ bool supports_adaptive_triggers = false; + + /** + * @brief Whether the profile supports player-indicator LED output. + */ + bool supports_player_led = false; + + /** + * @brief Whether the profile supports mic-mute LED output. + */ + bool supports_mic_led = false; }; /** @@ -998,6 +1008,8 @@ namespace lvh { adaptive_triggers, ///< Adaptive trigger output. raw_report, ///< Raw output report bytes. trigger_rumble, ///< Independent trigger rumble output. + player_led, ///< Player-indicator LED output. + mic_led, ///< Mic-mute LED output. }; /** @@ -1044,6 +1056,16 @@ namespace lvh { */ std::uint8_t blue = 0; + /** + * @brief Player-indicator LED bitmask. + */ + std::uint8_t player_led = 0; + + /** + * @brief Mic-mute LED state (0 = off, 1 = on, 2 = pulse). + */ + std::uint8_t mic_led = 0; + /** * @brief Adaptive trigger event flags from a profile-specific output report. */ diff --git a/tests/unit/test_gamepad_adapter.cpp b/tests/unit/test_gamepad_adapter.cpp index b94d1fe..2b9a60e 100644 --- a/tests/unit/test_gamepad_adapter.cpp +++ b/tests/unit/test_gamepad_adapter.cpp @@ -44,6 +44,8 @@ TEST(GamepadAdapterTest, ReportsProfileSupport) { EXPECT_TRUE(dualsense_support.supports_rumble); EXPECT_TRUE(dualsense_support.supports_rgb_led); EXPECT_TRUE(dualsense_support.supports_adaptive_triggers); + EXPECT_TRUE(dualsense_support.supports_player_led); + EXPECT_TRUE(dualsense_support.supports_mic_led); EXPECT_TRUE(dualsense_support.supports_motion); EXPECT_TRUE(dualsense_support.supports_touchpad); EXPECT_TRUE(dualsense_support.supports_battery); @@ -96,6 +98,8 @@ TEST(GamepadAdapterTest, ChecksButtonsAndOutputsByProfile) { EXPECT_FALSE(lvh::supports_gamepad_output(dualshock4, lvh::GamepadOutputKind::trigger_rumble)); EXPECT_TRUE(lvh::supports_gamepad_output(dualshock4, lvh::GamepadOutputKind::raw_report)); EXPECT_TRUE(lvh::supports_gamepad_output(dualsense, lvh::GamepadOutputKind::adaptive_triggers)); + EXPECT_TRUE(lvh::supports_gamepad_output(dualsense, lvh::GamepadOutputKind::player_led)); + EXPECT_TRUE(lvh::supports_gamepad_output(dualsense, lvh::GamepadOutputKind::mic_led)); EXPECT_TRUE(lvh::supports_gamepad_output(switch_pro, lvh::GamepadOutputKind::rumble)); EXPECT_TRUE(lvh::supports_gamepad_output(switch_pro, lvh::GamepadOutputKind::raw_report)); EXPECT_TRUE(lvh::supports_gamepad_output(generic, lvh::GamepadOutputKind::raw_report)); diff --git a/tests/unit/test_profiles.cpp b/tests/unit/test_profiles.cpp index 4b603bf..f8959c4 100644 --- a/tests/unit/test_profiles.cpp +++ b/tests/unit/test_profiles.cpp @@ -213,6 +213,8 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { EXPECT_TRUE(dualsense.capabilities.supports_touchpad); EXPECT_TRUE(dualsense.capabilities.supports_rgb_led); EXPECT_TRUE(dualsense.capabilities.supports_adaptive_triggers); + EXPECT_TRUE(dualsense.capabilities.supports_player_led); + EXPECT_TRUE(dualsense.capabilities.supports_mic_led); EXPECT_GT(dualsense.input_report_size, 14U); EXPECT_GT(dualsense.output_report_size, 5U); EXPECT_EQ(dualsense.manufacturer, "Sony Interactive Entertainment"); diff --git a/tests/unit/test_report.cpp b/tests/unit/test_report.cpp index 06a73a4..2b5cacc 100644 --- a/tests/unit/test_report.cpp +++ b/tests/unit/test_report.cpp @@ -525,6 +525,45 @@ TEST(ReportTest, ParsesDualSenseBluetoothOutputReportEvents) { EXPECT_EQ(outputs[2].kind, lvh::GamepadOutputKind::adaptive_triggers); } +TEST(ReportTest, ParsesDualSensePlayerAndMicLedOutputReportEvents) { + const auto profile = lvh::profiles::dualsense_usb(); + std::vector report(profile.output_report_size, 0); + report[0] = 0x02; + report[2] = 0x11; // valid_flag1: player-indicator (0x10) | mic-mute LED (0x01) + report[9] = 0x02; // mic-mute LED state (offset + 8): pulse + report[44] = 0x1F; // player-indicator LEDs (offset + 43): all five lit + + const auto outputs = lvh::reports::parse_output_reports(profile, report); + + ASSERT_EQ(outputs.size(), 2U); + EXPECT_EQ(outputs[0].kind, lvh::GamepadOutputKind::player_led); + EXPECT_EQ(outputs[0].player_led, 0x1F); + EXPECT_EQ(outputs[1].kind, lvh::GamepadOutputKind::mic_led); + EXPECT_EQ(outputs[1].mic_led, 0x02); +} + +TEST(ReportTest, OmitsDualSensePlayerAndMicLedWhenFlagsClear) { + const auto profile = lvh::profiles::dualsense_usb(); + std::vector report(profile.output_report_size, 0); + report[0] = 0x02; + report[1] = 0x01; // valid_flag0: rumble only + report[2] = 0x04; // valid_flag1: lightbar only (player/mic flags clear) + report[3] = 0x80; + report[4] = 0x40; + report[9] = 0x02; // mic-mute LED byte populated, but its valid flag is clear + report[44] = 0x1F; // player-indicator byte populated, but its valid flag is clear + report[45] = 0x11; + report[46] = 0x22; + report[47] = 0x33; + + const auto outputs = lvh::reports::parse_output_reports(profile, report); + + for (const auto &output : outputs) { + EXPECT_NE(output.kind, lvh::GamepadOutputKind::player_led); + EXPECT_NE(output.kind, lvh::GamepadOutputKind::mic_led); + } +} + TEST(ReportTest, ParsesDualShock4OutputReportEvents) { const auto profile = lvh::profiles::dualshock4_usb(); std::vector report(profile.output_report_size, 0); diff --git a/tests/unit/test_virtualhid_control_model.cpp b/tests/unit/test_virtualhid_control_model.cpp index e9c465e..eb16945 100644 --- a/tests/unit/test_virtualhid_control_model.cpp +++ b/tests/unit/test_virtualhid_control_model.cpp @@ -58,6 +58,8 @@ TEST(VirtualHidControlModelTest, NamesKnownAndFallbackEnumValues) { EXPECT_EQ(control::output_kind_name(lvh::GamepadOutputKind::adaptive_triggers), L"adaptive triggers"); EXPECT_EQ(control::output_kind_name(lvh::GamepadOutputKind::raw_report), L"raw report"); EXPECT_EQ(control::output_kind_name(lvh::GamepadOutputKind::trigger_rumble), L"trigger rumble"); + EXPECT_EQ(control::output_kind_name(lvh::GamepadOutputKind::player_led), L"player led"); + EXPECT_EQ(control::output_kind_name(lvh::GamepadOutputKind::mic_led), L"mic led"); EXPECT_EQ(control::output_kind_name(static_cast(255)), L"raw report"); EXPECT_EQ(control::battery_state_name(lvh::GamepadBatteryState::unknown), L"unknown"); diff --git a/tools/virtualhid_control_model.cpp b/tools/virtualhid_control_model.cpp index d5eb8af..61f3e00 100644 --- a/tools/virtualhid_control_model.cpp +++ b/tools/virtualhid_control_model.cpp @@ -77,6 +77,10 @@ namespace lvh::tools::virtualhid_control { return L"raw report"; case trigger_rumble: return L"trigger rumble"; + case player_led: + return L"player led"; + case mic_led: + return L"mic led"; } return L"raw report"; } From 623b214b87a4c06a5b196ec94e199898d8c28665 Mon Sep 17 00:00:00 2001 From: Joel Coelho Date: Thu, 20 Aug 2026 13:41:07 +0100 Subject: [PATCH 2/2] fix: add latest_player_led/latest_mic_led switch cases and tests Signed-off-by: Joel Coelho --- tests/unit/test_virtualhid_control_model.cpp | 24 +++++++++++++++++++- tools/virtualhid_control_model.cpp | 16 +++++++++++++ tools/virtualhid_control_model.hpp | 2 ++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_virtualhid_control_model.cpp b/tests/unit/test_virtualhid_control_model.cpp index eb16945..261f789 100644 --- a/tests/unit/test_virtualhid_control_model.cpp +++ b/tests/unit/test_virtualhid_control_model.cpp @@ -198,12 +198,16 @@ TEST(VirtualHidControlModelTest, SummarizesOutputState) { state.latest_rgb_led->red = 1; state.latest_rgb_led->green = 2; state.latest_rgb_led->blue = 3; + state.latest_player_led = output(player_led); + state.latest_player_led->player_led = 31; + state.latest_mic_led = output(mic_led); + state.latest_mic_led->mic_led = 2; state.latest_adaptive_triggers = output(adaptive_triggers); state.latest_adaptive_triggers->adaptive_trigger_flags = 4; EXPECT_EQ( control::output_summary(state, dualsense), - L"Output: rumble low=10 high=20 | trigger rumble L=30 R=40 | RGB 1,2,3 | adaptive flags=4" + L"Output: rumble low=10 high=20 | trigger rumble L=30 R=40 | RGB 1,2,3 | player LEDs=31 | mic LED=2 | adaptive flags=4" ); } @@ -251,3 +255,21 @@ TEST(VirtualHidControlModelTest, RecordsOutputsAndMaintainsLatestSummaryFields) ASSERT_TRUE(state.latest_raw_report.has_value()); EXPECT_EQ(state.latest_raw_report->raw_report, (std::vector {0x12, 0x34})); } + +TEST(VirtualHidControlModelTest, RecordsPlayerAndMicLedOutputs) { + control::OutputState state; + auto next_sequence = std::uint64_t {1}; + + auto player = output(lvh::GamepadOutputKind::player_led); + player.player_led = 0x1F; + control::record_output(state, player, next_sequence, 8); + + auto mic = output(lvh::GamepadOutputKind::mic_led); + mic.mic_led = 2; + control::record_output(state, mic, next_sequence, 8); + + ASSERT_TRUE(state.latest_player_led.has_value()); + EXPECT_EQ(state.latest_player_led->player_led, 0x1F); + ASSERT_TRUE(state.latest_mic_led.has_value()); + EXPECT_EQ(state.latest_mic_led->mic_led, 2); +} diff --git a/tools/virtualhid_control_model.cpp b/tools/virtualhid_control_model.cpp index 61f3e00..b183656 100644 --- a/tools/virtualhid_control_model.cpp +++ b/tools/virtualhid_control_model.cpp @@ -215,6 +215,16 @@ namespace lvh::tools::virtualhid_control { << static_cast(state.latest_rgb_led->blue); wrote = true; } + if (state.latest_player_led) { + append_summary_separator(stream, wrote); + stream << L"player LEDs=" << static_cast(state.latest_player_led->player_led); + wrote = true; + } + if (state.latest_mic_led) { + append_summary_separator(stream, wrote); + stream << L"mic LED=" << static_cast(state.latest_mic_led->mic_led); + wrote = true; + } if (state.latest_adaptive_triggers) { append_summary_separator(stream, wrote); stream << L"adaptive flags=" << static_cast(state.latest_adaptive_triggers->adaptive_trigger_flags); @@ -284,6 +294,12 @@ namespace lvh::tools::virtualhid_control { case rgb_led: state.latest_rgb_led = output; break; + case player_led: + state.latest_player_led = output; + break; + case mic_led: + state.latest_mic_led = output; + break; case adaptive_triggers: state.latest_adaptive_triggers = output; break; diff --git a/tools/virtualhid_control_model.hpp b/tools/virtualhid_control_model.hpp index 502a160..6dd7986 100644 --- a/tools/virtualhid_control_model.hpp +++ b/tools/virtualhid_control_model.hpp @@ -55,6 +55,8 @@ namespace lvh::tools::virtualhid_control { std::optional latest_rumble; std::optional latest_trigger_rumble; std::optional latest_rgb_led; + std::optional latest_player_led; + std::optional latest_mic_led; std::optional latest_adaptive_triggers; std::optional latest_raw_report; };