mirror of
https://github.com/LizardByte/libvirtualhid.git
synced 2026-10-23 03:27:34 +09:00
feat(gamepad): improve Switch Pro motion and LED support (#114)
This commit is contained in:
1 parent
61c0c19721
commit
70f98435ba
33 files changed
+1166
-264
No files matched your search
@@ -181,9 +181,8 @@ jobs:
|
|||||||
echo "::warning::${kernel_modules_package} is unavailable; relying on the runner image kernel modules."
|
echo "::warning::${kernel_modules_package} is unavailable; relying on the runner image kernel modules."
|
||||||
fi
|
fi
|
||||||
sudo tee /etc/udev/rules.d/99-libvirtualhid-ci.rules >/dev/null <<'EOF'
|
sudo tee /etc/udev/rules.d/99-libvirtualhid-ci.rules >/dev/null <<'EOF'
|
||||||
SUBSYSTEM=="hidraw", KERNEL=="hidraw*", ATTRS{phys}=="libvirtualhid/uhid/*", MODE="0666", TAG+="uaccess"
|
SUBSYSTEM=="hidraw", KERNEL=="hidraw*", MODE="0666", TAG+="uaccess"
|
||||||
SUBSYSTEM=="input", KERNEL=="event*", ATTRS{phys}=="libvirtualhid/uhid/*", MODE="0666", TAG+="uaccess"
|
SUBSYSTEM=="input", KERNEL=="event*", ATTRS{phys}=="libvirtualhid/uhid/*", MODE="0666", TAG+="uaccess"
|
||||||
SUBSYSTEM=="hidraw", KERNEL=="hidraw*", ATTRS{name}=="(libvirtualhid)*", MODE="0666", TAG+="uaccess"
|
|
||||||
SUBSYSTEM=="input", KERNEL=="event*", ATTRS{name}=="(libvirtualhid)*", MODE="0666", TAG+="uaccess"
|
SUBSYSTEM=="input", KERNEL=="event*", ATTRS{name}=="(libvirtualhid)*", MODE="0666", TAG+="uaccess"
|
||||||
SUBSYSTEM=="input", KERNEL=="event*", ATTRS{name}=="libvirtualhid*", MODE="0666", TAG+="uaccess"
|
SUBSYSTEM=="input", KERNEL=="event*", ATTRS{name}=="libvirtualhid*", MODE="0666", TAG+="uaccess"
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="docs" type="CMakeRunConfiguration" factoryName="Application" PROGRAM_PARAMS="file:///$PROJECT_DIR$/../tray/cmake-build-release/docs/build/html/index.html" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="libvirtualhid" TARGET_NAME="docs" CONFIG_NAME="Release-MinGW (ucrt64-ninja)" RUN_PATH="$PROJECT_DIR$/../../../../../Windows/explorer.exe">
|
<configuration default="false" name="docs" type="CMakeRunConfiguration" factoryName="Application" PROGRAM_PARAMS="file:///$PROJECT_DIR$/cmake-build-release-mingw-ucrt64-ninja/docs/build/html/index.html" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="libvirtualhid" TARGET_NAME="docs" CONFIG_NAME="Release-MinGW (ucrt64-ninja)" RUN_PATH="$PROJECT_DIR$/../../../../../Windows/explorer.exe">
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
||||||
</method>
|
</method>
|
||||||
|
|||||||
@@ -45,14 +45,14 @@ behind backend implementations.
|
|||||||
|
|
||||||
- Gamepad profiles for generic HID, Xbox 360, Xbox One, Xbox Series,
|
- Gamepad profiles for generic HID, Xbox 360, Xbox One, Xbox Series,
|
||||||
DualShock 4, DualSense, and Nintendo Switch Pro-style controllers.
|
DualShock 4, DualSense, and Nintendo Switch Pro-style controllers.
|
||||||
- Descriptor-driven PlayStation gamepads through Linux `uhid`; Generic, Xbox,
|
- Descriptor-driven PlayStation and Switch Pro gamepads through Linux `uhid`;
|
||||||
and Switch Pro gamepads plus keyboard, mouse, touchscreen, trackpad, and pen
|
Generic and Xbox gamepads plus keyboard, mouse, touchscreen, trackpad, and
|
||||||
tablet devices through `uinput`.
|
pen tablet devices through `uinput`.
|
||||||
- Windows gamepads, keyboards, and Raw Input-visible mice through a user-mode
|
- Windows gamepads, keyboards, and Raw Input-visible mice through a user-mode
|
||||||
UMDF2 control driver backed by Virtual HID Framework, with Win32 keyboard and
|
UMDF2 control driver backed by Virtual HID Framework, with Win32 keyboard and
|
||||||
mouse fallbacks when the licensed driver path is unavailable.
|
mouse fallbacks when the licensed driver path is unavailable.
|
||||||
- Output callbacks for profile-specific feedback such as rumble, LEDs,
|
- Output callbacks for profile-specific feedback such as rumble, RGB and player
|
||||||
adaptive triggers, and raw HID output reports when available.
|
LEDs, adaptive triggers, and raw HID output reports when available.
|
||||||
- An optional `virtualhid_control` native UI tool for creating, removing,
|
- An optional `virtualhid_control` native UI tool for creating, removing,
|
||||||
controlling, and inspecting test gamepads and mice through the public C++ API.
|
controlling, and inspecting test gamepads and mice through the public C++ API.
|
||||||
- CMake consumption through installed packages, vendored source,
|
- CMake consumption through installed packages, vendored source,
|
||||||
|
|||||||
@@ -113,34 +113,34 @@ columns compare the existing Windows Virtual HID Driver results.
|
|||||||
| Standard buttons, sticks, and D-pad | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
| Standard buttons, sticks, and D-pad | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
| Analog trigger input (0 to 1) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
| Analog trigger input (0 to 1) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
| Basic rumble | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
| Basic rumble | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
| Impulse Triggers | ✅ | ❌<sup><a href="#compatibility-note-11">11</a></sup> | ✅ | ❌ | ✅<sup><a href="#compatibility-note-15">15</a></sup> | ✅ |
|
| Impulse Triggers | ✅ | ❌<sup><a href="#compatibility-note-10">10</a></sup> | ✅ | ❌ | ✅<sup><a href="#compatibility-note-14">14</a></sup> | ✅ |
|
||||||
| Battery state | ❌<sup><a href="#compatibility-note-4">4</a></sup> | ❌<sup><a href="#compatibility-note-4">4</a></sup> | ❌<sup><a href="#compatibility-note-4">4</a></sup> | ❌<sup><a href="#compatibility-note-4">4</a></sup> | ❌<sup><a href="#compatibility-note-4">4</a></sup> | ❌<sup><a href="#compatibility-note-4">4</a></sup> |
|
| Battery state | ❌<sup><a href="#compatibility-note-4">4</a></sup> | ❌<sup><a href="#compatibility-note-4">4</a></sup> | ❌<sup><a href="#compatibility-note-4">4</a></sup> | ❌<sup><a href="#compatibility-note-4">4</a></sup> | ❌<sup><a href="#compatibility-note-4">4</a></sup> | ❌<sup><a href="#compatibility-note-4">4</a></sup> |
|
||||||
| **Xbox Series** | | | | | | |
|
| **Xbox Series** | | | | | | |
|
||||||
| Standard buttons, sticks, and D-pad | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
| Standard buttons, sticks, and D-pad | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
| Analog trigger input (0 to 1) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
| Analog trigger input (0 to 1) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
| Basic rumble | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
| Basic rumble | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
| Impulse Triggers | ✅ | ❌<sup><a href="#compatibility-note-11">11</a></sup> | ✅ | ❌ | ✅<sup><a href="#compatibility-note-15">15</a></sup> | ✅ |
|
| Impulse Triggers | ✅ | ❌<sup><a href="#compatibility-note-10">10</a></sup> | ✅ | ❌ | ✅<sup><a href="#compatibility-note-14">14</a></sup> | ✅ |
|
||||||
| Battery state | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
| Battery state | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||||
| Share button | ❌<sup><a href="#compatibility-note-3">3</a></sup> | ❌<sup><a href="#compatibility-note-8">8</a></sup> | ❌<sup><a href="#compatibility-note-3">3</a></sup> | ❌<sup><a href="#compatibility-note-3">3</a></sup> | ❌<sup><a href="#compatibility-note-3">3</a></sup> | ❌<sup><a href="#compatibility-note-3">3</a></sup> |
|
| Share button | ❌<sup><a href="#compatibility-note-3">3</a></sup> | ❌<sup><a href="#compatibility-note-8">8</a></sup> | ❌<sup><a href="#compatibility-note-3">3</a></sup> | ❌<sup><a href="#compatibility-note-3">3</a></sup> | ❌<sup><a href="#compatibility-note-3">3</a></sup> | ❌<sup><a href="#compatibility-note-3">3</a></sup> |
|
||||||
| **DualShock 4** | | | | | | |
|
| **DualShock 4** | | | | | | |
|
||||||
| Standard buttons, sticks, and D-pad | ✅ | ✅ | ✅ | ✅ | ✅ | ➖ |
|
| Standard buttons, sticks, and D-pad | ✅ | ✅ | ✅ | ✅ | ✅ | ➖ |
|
||||||
| Analog trigger input (0 to 1) | ✅ | ✅ | ✅ | ✅ | ✅ | ➖ |
|
| Analog trigger input (0 to 1) | ✅ | ✅ | ✅ | ✅ | ✅ | ➖ |
|
||||||
| Basic rumble | ✅ | ✅ | ✅ | ✅<sup><a href="#compatibility-note-5">5</a></sup> | ✅ | ➖ |
|
| Basic rumble | ✅ | ✅ | ✅ | ✅<sup><a href="#compatibility-note-5">5</a></sup> | ✅ | ➖ |
|
||||||
| Motion/gyro | ✅ | ✅ | ✅ | ✅<sup><a href="#compatibility-note-1">1</a></sup> | ✅<sup><a href="#compatibility-note-15">15</a></sup> | ➖ |
|
| Motion/gyro | ✅ | ✅ | ✅ | ✅<sup><a href="#compatibility-note-1">1</a></sup> | ✅<sup><a href="#compatibility-note-14">14</a></sup> | ➖ |
|
||||||
| Touchpad position | ✅ | ✅ | ✅ | ❌<sup><a href="#compatibility-note-2">2</a></sup> | ✅<sup><a href="#compatibility-note-15">15</a></sup> | ➖ |
|
| Touchpad position | ✅ | ✅ | ✅ | ❌<sup><a href="#compatibility-note-2">2</a></sup> | ✅<sup><a href="#compatibility-note-14">14</a></sup> | ➖ |
|
||||||
| Touchpad click | ✅ | ✅ | ✅ | ❌<sup><a href="#compatibility-note-2">2</a></sup> | ✅<sup><a href="#compatibility-note-15">15</a></sup> | ➖ |
|
| Touchpad click | ✅ | ✅ | ✅ | ❌<sup><a href="#compatibility-note-2">2</a></sup> | ✅<sup><a href="#compatibility-note-14">14</a></sup> | ➖ |
|
||||||
| Light bar (RGB/player color) | ✅ | ✅ | ✅ | ✅<sup><a href="#compatibility-note-6">6</a></sup> | ✅<sup><a href="#compatibility-note-15">15</a></sup> | ➖ |
|
| Light bar (RGB/player color) | ✅ | ✅ | ✅ | ✅<sup><a href="#compatibility-note-6">6</a></sup> | ✅<sup><a href="#compatibility-note-14">14</a></sup> | ➖ |
|
||||||
| Battery state | ❌ | ✅ | ✅ | ✅ | ✅ | ➖ |
|
| Battery state | ❌ | ✅ | ✅ | ✅ | ✅ | ➖ |
|
||||||
| **DualSense** | | | | | | |
|
| **DualSense** | | | | | | |
|
||||||
| Standard buttons, sticks, and D-pad | ✅ | ✅ | ✅ | ✅ | ✅ | ➖ |
|
| Standard buttons, sticks, and D-pad | ✅ | ✅ | ✅ | ✅ | ✅ | ➖ |
|
||||||
| Analog trigger input (0 to 1) | ✅ | ✅ | ✅ | ✅ | ✅ | ➖ |
|
| Analog trigger input (0 to 1) | ✅ | ✅ | ✅ | ✅ | ✅ | ➖ |
|
||||||
| Basic rumble | ✅ | ✅ | ✅ | ✅<sup><a href="#compatibility-note-5">5</a></sup> | ✅ | ➖ |
|
| Basic rumble | ✅ | ✅ | ✅ | ✅<sup><a href="#compatibility-note-5">5</a></sup> | ✅ | ➖ |
|
||||||
| Motion/gyro | ✅ | ✅ | ✅ | ✅<sup><a href="#compatibility-note-1">1</a></sup> | ❌ | ➖ |
|
| Motion/gyro | ✅ | ✅ | ✅ | ✅<sup><a href="#compatibility-note-1">1</a></sup> | ❌ | ➖ |
|
||||||
| Touchpad position | ✅ | ✅ | ✅ | ❌<sup><a href="#compatibility-note-2">2</a></sup> | ✅<sup><a href="#compatibility-note-15">15</a></sup> | ➖ |
|
| Touchpad position | ✅ | ✅ | ✅ | ❌<sup><a href="#compatibility-note-2">2</a></sup> | ✅<sup><a href="#compatibility-note-14">14</a></sup> | ➖ |
|
||||||
| Touchpad click | ✅ | ✅ | ✅ | ❌<sup><a href="#compatibility-note-2">2</a></sup> | ✅<sup><a href="#compatibility-note-15">15</a></sup> | ➖ |
|
| Touchpad click | ✅ | ✅ | ✅ | ❌<sup><a href="#compatibility-note-2">2</a></sup> | ✅<sup><a href="#compatibility-note-14">14</a></sup> | ➖ |
|
||||||
| Light bar (RGB) | ✅ | ✅ | ✅ | ✅<sup><a href="#compatibility-note-6">6</a></sup> | ✅<sup><a href="#compatibility-note-15">15</a></sup> | ➖ |
|
| Light bar (RGB) | ✅ | ✅ | ✅ | ✅<sup><a href="#compatibility-note-6">6</a></sup> | ✅<sup><a href="#compatibility-note-14">14</a></sup> | ➖ |
|
||||||
| Battery state | ❌ | ✅ | ✅ | ✅ | ✅ | ➖ |
|
| Battery state | ❌ | ✅ | ✅ | ✅ | ✅ | ➖ |
|
||||||
| Adaptive triggers | ❓<sup><a href="#compatibility-note-12">12</a></sup> | ❓<sup><a href="#compatibility-note-12">12</a></sup> | ❓<sup><a href="#compatibility-note-12">12</a></sup> | ❌<sup><a href="#compatibility-note-2">2</a></sup> | ❓ | ➖ |
|
| Adaptive triggers | ❓<sup><a href="#compatibility-note-11">11</a></sup> | ✅ | ❓<sup><a href="#compatibility-note-11">11</a></sup> | ❌<sup><a href="#compatibility-note-2">2</a></sup> | ❓ | ➖ |
|
||||||
| Player indicator | ❌<sup><a href="#compatibility-note-7">7</a></sup> | ❌<sup><a href="#compatibility-note-7">7</a></sup> | ❌<sup><a href="#compatibility-note-7">7</a></sup> | ❌<sup><a href="#compatibility-note-7">7</a></sup> | ❌ | ➖ |
|
| Player indicator | ❌<sup><a href="#compatibility-note-7">7</a></sup> | ❌<sup><a href="#compatibility-note-7">7</a></sup> | ❌<sup><a href="#compatibility-note-7">7</a></sup> | ❌<sup><a href="#compatibility-note-7">7</a></sup> | ❌ | ➖ |
|
||||||
| MUTE button | ✅ | ✅ | ✅ | ❌ | ❌ | ➖ |
|
| MUTE button | ✅ | ✅ | ✅ | ❌ | ❌ | ➖ |
|
||||||
| MUTE button LED | ❌<sup><a href="#compatibility-note-7">7</a></sup> | ❌<sup><a href="#compatibility-note-7">7</a></sup> | ❌<sup><a href="#compatibility-note-7">7</a></sup> | ❌<sup><a href="#compatibility-note-7">7</a></sup> | ❌ | ➖ |
|
| MUTE button LED | ❌<sup><a href="#compatibility-note-7">7</a></sup> | ❌<sup><a href="#compatibility-note-7">7</a></sup> | ❌<sup><a href="#compatibility-note-7">7</a></sup> | ❌<sup><a href="#compatibility-note-7">7</a></sup> | ❌ | ➖ |
|
||||||
@@ -148,32 +148,33 @@ columns compare the existing Windows Virtual HID Driver results.
|
|||||||
| Standard buttons, sticks, and D-pad | ✅ | ✅ | ✅ | ✅ | ✅ | ➖ |
|
| Standard buttons, sticks, and D-pad | ✅ | ✅ | ✅ | ✅ | ✅ | ➖ |
|
||||||
| Digital trigger input (0 or 1) | ✅ | ✅ | ✅ | ✅ | ✅ | ➖ |
|
| Digital trigger input (0 or 1) | ✅ | ✅ | ✅ | ✅ | ✅ | ➖ |
|
||||||
| Basic rumble | ✅ | ✅ | ✅ | ❌ | ❌ | ➖ |
|
| Basic rumble | ✅ | ✅ | ✅ | ❌ | ❌ | ➖ |
|
||||||
| Motion/gyro | ❌<sup><a href="#compatibility-note-9">9</a></sup> | ❌<sup><a href="#compatibility-note-13">13</a></sup> | ❌<sup><a href="#compatibility-note-9">9</a></sup> | ❌<sup><a href="#compatibility-note-9">9</a></sup> | ❌<sup><a href="#compatibility-note-9">9</a></sup> | ➖ |
|
| Motion/gyro | ✅ | ✅ | ✅ | ✅ | ✅ | ➖ |
|
||||||
| Battery state | ❌ | ❌ | ❌ | ❌ | ❌ | ➖ |
|
| Battery state | ❌ | ✅ | ✅ | ✅ | ❌ | ➖ |
|
||||||
| Player LED | ❌<sup><a href="#compatibility-note-14">14</a></sup> | ❌<sup><a href="#compatibility-note-14">14</a></sup> | ❌<sup><a href="#compatibility-note-14">14</a></sup> | ❌<sup><a href="#compatibility-note-14">14</a></sup> | ❌<sup><a href="#compatibility-note-14">14</a></sup> | ➖ |
|
| HOME LED | ❌<sup><a href="#compatibility-note-15">15</a></sup> | ❌<sup><a href="#compatibility-note-15">15</a></sup> | ❌<sup><a href="#compatibility-note-15">15</a></sup> | ❌ | ❌ | ➖ |
|
||||||
| Capture button | ✅ | ✅ | ✅ | ❌<sup><a href="#compatibility-note-10">10</a></sup> | ✅<sup><a href="#compatibility-note-15">15</a></sup> | ➖ |
|
| Player LED | ❌<sup><a href="#compatibility-note-13">13</a></sup> | ❌<sup><a href="#compatibility-note-13">13</a></sup> | ❌<sup><a href="#compatibility-note-13">13</a></sup> | ❌<sup><a href="#compatibility-note-13">13</a></sup> | ❌<sup><a href="#compatibility-note-13">13</a></sup> | ➖ |
|
||||||
|
| Capture button | ✅ | ✅ | ✅ | ❌<sup><a href="#compatibility-note-9">9</a></sup> | ✅<sup><a href="#compatibility-note-14">14</a></sup> | ➖ |
|
||||||
|
|
||||||
When a backend is marked ❌, that path cannot establish whether an additional
|
When a backend is marked ❌, that path cannot establish whether an additional
|
||||||
client-side limitation exists. The owner below identifies the first known layer
|
client-side limitation exists. The owner below identifies the first known layer
|
||||||
that prevents the feature from working end to end.
|
that prevents the feature from working end to end.
|
||||||
|
|
||||||
| Note | Owner | Limitation or status | Tracker or reference |
|
| Note | Owner | Limitation or status | Tracker or reference |
|
||||||
|-------------------------------------------------|----------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-------------------------------------------------|----------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| <a id="compatibility-note-1"></a><sup>1</sup> | Client platform | Moonlight Android exposes gamepad motion on Android 12 or later when motion is enabled and the Android device exposes the controller sensors. Available settings can differ between devices. | [Moonlight Android motion settings](https://github.com/moonlight-stream/moonlight-android/blob/f10085f552b367cf7203007693d91c322a0a2936/app/src/main/java/com/limelight/preferences/StreamSettings.java#L296-L309) |
|
| <a id="compatibility-note-1"></a><sup>1</sup> | Client platform | Moonlight Android exposes gamepad motion on Android 12 or later when motion is enabled and the Android device exposes the controller sensors. Available settings can differ between devices. | [Moonlight Android motion settings](https://github.com/moonlight-stream/moonlight-android/blob/f10085f552b367cf7203007693d91c322a0a2936/app/src/main/java/com/limelight/preferences/StreamSettings.java#L296-L309) |
|
||||||
| <a id="compatibility-note-2"></a><sup>2</sup> | Client platform | Android may expose a PlayStation touchpad as a mouse instead of a native controller touchpad. Leave **Gamepad touchpad as mouse** disabled when native forwarding is available. DualSense support requires Android 12 or later, and Sony documents that adaptive triggers are unavailable on Android mobile devices. | [Sony Android requirements](https://www.playstation.com/en-us/support/hardware/pair-dualsense-controller-bluetooth/) and [Moonlight Android touchpad handling](https://github.com/moonlight-stream/moonlight-android/blob/f10085f552b367cf7203007693d91c322a0a2936/app/src/main/java/com/limelight/binding/input/ControllerHandler.java#L1680-L1778) |
|
| <a id="compatibility-note-2"></a><sup>2</sup> | Client platform | Android may expose a PlayStation touchpad as a mouse instead of a native controller touchpad. Leave **Gamepad touchpad as mouse** disabled when native forwarding is available. DualSense support requires Android 12 or later, and Sony documents that adaptive triggers are unavailable on Android mobile devices. | [Sony Android requirements](https://www.playstation.com/en-us/support/hardware/pair-dualsense-controller-bluetooth/) and [Moonlight Android touchpad handling](https://github.com/moonlight-stream/moonlight-android/blob/f10085f552b367cf7203007693d91c322a0a2936/app/src/main/java/com/limelight/binding/input/ControllerHandler.java#L1680-L1778) |
|
||||||
| <a id="compatibility-note-3"></a><sup>3</sup> | Windows host backend | Steam does not expose the Xbox Series Share button through Virtual HID Driver on Windows. | [libvirtualhid issue #106](https://github.com/LizardByte/libvirtualhid/issues/106) |
|
| <a id="compatibility-note-3"></a><sup>3</sup> | Windows host backend | Steam does not expose the Xbox Series Share button through Virtual HID Driver on Windows. | [libvirtualhid issue #106](https://github.com/LizardByte/libvirtualhid/issues/106) |
|
||||||
| <a id="compatibility-note-4"></a><sup>4</sup> | Host profile | The Xbox One profile rejects battery updates independently of the client. | [libvirtualhid issue #107](https://github.com/LizardByte/libvirtualhid/issues/107) |
|
| <a id="compatibility-note-4"></a><sup>4</sup> | Host profile | The Xbox One profile rejects battery updates independently of the client. | [libvirtualhid issue #107](https://github.com/LizardByte/libvirtualhid/issues/107) |
|
||||||
| <a id="compatibility-note-5"></a><sup>5</sup> | Client platform and external consumer | Android rumble depends on device vibration APIs and compatible motors. Steam may not dispatch PlayStation rumble until its controller settings or calibration page initializes the controller. | [Moonlight Android vibration handling](https://github.com/moonlight-stream/moonlight-android/blob/master/app/src/main/java/com/limelight/binding/input/ControllerHandler.java#L3373-L3419), [libvirtualhid issue #80](https://github.com/LizardByte/libvirtualhid/issues/80), and [Steam for Linux issue #13435](https://github.com/ValveSoftware/steam-for-linux/issues/13435) |
|
| <a id="compatibility-note-5"></a><sup>5</sup> | Client platform and external consumer | Android rumble depends on device vibration APIs and compatible motors. Steam may not dispatch PlayStation rumble until its controller settings or calibration page initializes the controller. | [Moonlight Android vibration handling](https://github.com/moonlight-stream/moonlight-android/blob/master/app/src/main/java/com/limelight/binding/input/ControllerHandler.java#L3373-L3419), [libvirtualhid issue #80](https://github.com/LizardByte/libvirtualhid/issues/80), and [Steam for Linux issue #13435](https://github.com/ValveSoftware/steam-for-linux/issues/13435) |
|
||||||
| <a id="compatibility-note-6"></a><sup>6</sup> | Client platform | Moonlight Android uses the RGB lights API available on Android 12 or later. It worked on tested newer devices but was unavailable on NVIDIA Shield running Android 11. | [Moonlight Android RGB-light detection](https://github.com/moonlight-stream/moonlight-android/blob/master/app/src/main/java/com/limelight/binding/input/ControllerHandler.java#L3454-L3470) |
|
| <a id="compatibility-note-6"></a><sup>6</sup> | Client platform | Moonlight Android uses the RGB lights API available on Android 12 or later. It worked on tested newer devices but was unavailable on NVIDIA Shield running Android 11. | [Moonlight Android RGB-light detection](https://github.com/moonlight-stream/moonlight-android/blob/master/app/src/main/java/com/limelight/binding/input/ControllerHandler.java#L3454-L3470) |
|
||||||
| <a id="compatibility-note-7"></a><sup>7</sup> | Host output pipeline | DualSense player-indicator and MUTE-button LED forwarding is covered by open host pull requests. This note applies only to the LEDs; the MUTE button input works through both host backends with Moonlight Qt. When a DualSense is connected to Android, its physical MUTE-button LED works locally, but that LED state is not forwarded to the virtual controller on the host. | [libvirtualhid pull request #97](https://github.com/LizardByte/libvirtualhid/pull/97) and [Sunshine pull request #5537](https://github.com/LizardByte/Sunshine/pull/5537) |
|
| <a id="compatibility-note-7"></a><sup>7</sup> | Host output pipeline | DualSense player-indicator and MUTE-button LED forwarding is covered by open host pull requests. This note applies only to the LEDs; the MUTE button input works through both host backends with Moonlight Qt. When a DualSense is connected to Android, its physical MUTE-button LED works locally, but that LED state is not forwarded to the virtual controller on the host. | [libvirtualhid pull request #97](https://github.com/LizardByte/libvirtualhid/pull/97) and [Sunshine pull request #5537](https://github.com/LizardByte/Sunshine/pull/5537) |
|
||||||
| <a id="compatibility-note-8"></a><sup>8</sup> | Linux host backend | Steam exposes the Xbox Series Share button through the Linux uinput device, but pressing it does not change the button state. | [libvirtualhid issue #110](https://github.com/LizardByte/libvirtualhid/issues/110) |
|
| <a id="compatibility-note-8"></a><sup>8</sup> | Linux host backend | Steam exposes the Xbox Series Share button through the Linux uinput device, but pressing it does not change the button state. | [libvirtualhid issue #110](https://github.com/LizardByte/libvirtualhid/issues/110) |
|
||||||
| <a id="compatibility-note-9"></a><sup>9</sup> | Windows host backend and Android client | On Windows, Steam exposes Switch Pro gyro input but its values remain static. The host behavior is tracked in libvirtualhid; the Android client separately tracks exposing Switch Pro with gyro. | [libvirtualhid issue #111](https://github.com/LizardByte/libvirtualhid/issues/111) and [Moonlight Android issue #1497](https://github.com/moonlight-stream/moonlight-android/issues/1497) |
|
| <a id="compatibility-note-9"></a><sup>9</sup> | Client | Moonlight Android exposes the tested Switch Pro Capture input as A instead of Capture. A broader Android Switch Pro mapping issue exists, but the exact Capture symptom is not explicitly tracked. | [Moonlight Android issue #842](https://github.com/moonlight-stream/moonlight-android/issues/842) |
|
||||||
| <a id="compatibility-note-10"></a><sup>10</sup> | Client | Moonlight Android exposes the tested Switch Pro Capture input as A instead of Capture. A broader Android Switch Pro mapping issue exists, but the exact Capture symptom is not explicitly tracked. | [Moonlight Android issue #842](https://github.com/moonlight-stream/moonlight-android/issues/842) |
|
| <a id="compatibility-note-10"></a><sup>10</sup> | Host backend | The Linux uinput/evdev Xbox One and Xbox Series profiles support basic rumble but cannot deliver independent Impulse Triggers. | [libvirtualhid issue #109](https://github.com/LizardByte/libvirtualhid/issues/109) |
|
||||||
| <a id="compatibility-note-11"></a><sup>11</sup> | Host backend | The Linux uinput/evdev Xbox One and Xbox Series profiles support basic rumble but cannot deliver independent Impulse Triggers. | [libvirtualhid issue #109](https://github.com/LizardByte/libvirtualhid/issues/109) |
|
| <a id="compatibility-note-11"></a><sup>11</sup> | Client and protocol; resolved upstream, unreleased | Moonlight Qt adaptive-trigger support and its protocol and Sunshine dependencies are merged, but the latest published Moonlight Qt release predates them. | [Moonlight Qt pull request #1561](https://github.com/moonlight-stream/moonlight-qt/pull/1561), [moonlight-common-c pull request #102](https://github.com/moonlight-stream/moonlight-common-c/pull/102), [Sunshine pull request #3738](https://github.com/LizardByte/Sunshine/pull/3738), and [Moonlight Qt v6.1.0](https://github.com/moonlight-stream/moonlight-qt/releases/tag/v6.1.0) |
|
||||||
| <a id="compatibility-note-12"></a><sup>12</sup> | Client and protocol; resolved upstream, unreleased | Moonlight Qt adaptive-trigger support and its protocol and Sunshine dependencies are merged, but the latest published Moonlight Qt release predates them. The feature has not yet been validated with these backends. | [Moonlight Qt pull request #1561](https://github.com/moonlight-stream/moonlight-qt/pull/1561), [moonlight-common-c pull request #102](https://github.com/moonlight-stream/moonlight-common-c/pull/102), [Sunshine pull request #3738](https://github.com/LizardByte/Sunshine/pull/3738), and [Moonlight Qt v6.1.0](https://github.com/moonlight-stream/moonlight-qt/releases/tag/v6.1.0) |
|
| <a id="compatibility-note-12"></a><sup>12</sup> | Linux host backend | The Linux backend uses descriptor-driven UHID for Switch Pro, advertises a backend-only Bluetooth transport identity that SDL2 HIDAPI accepts for virtual devices, answers its initialization protocol, and carries live native motion reports. Motion and battery were validated end to end from Moonlight Qt v6.1.0 on Windows through Sunshine on Linux into Steam. | [libvirtualhid issue #112](https://github.com/LizardByte/libvirtualhid/issues/112) and [closed Sunshine issue #3838](https://github.com/LizardByte/Sunshine/issues/3838) |
|
||||||
| <a id="compatibility-note-13"></a><sup>13</sup> | Linux host backend | Steam does not expose Switch Pro gyro because the current Linux uinput route cannot carry the profile's native motion reports. The libvirtualhid issue supersedes the closed, pre-libvirtualhid Sunshine request. | [libvirtualhid issue #112](https://github.com/LizardByte/libvirtualhid/issues/112) and [closed Sunshine issue #3838](https://github.com/LizardByte/Sunshine/issues/3838) |
|
| <a id="compatibility-note-13"></a><sup>13</sup> | Streaming host and client output pipeline | Both host backends decode Switch Pro Set Player Lights output into solid and flashing player-indicator callbacks, and Sunshine can serialize those masks through its proposed protocol extension. Released moonlight-common-c and Moonlight clients do not consume that extension, so testing with Moonlight Qt v6.1.0 leaves the physical player LEDs unchanged on both host backends. | [libvirtualhid issue #113](https://github.com/LizardByte/libvirtualhid/issues/113) and [Sunshine player-LED integration](https://github.com/LizardByte/Sunshine/commit/596fbf9dc53775de87bc383a5293d4fcd546f837) |
|
||||||
| <a id="compatibility-note-14"></a><sup>14</sup> | Host output pipeline | Switch Pro Player LED output is not decoded and forwarded by either host backend. | [libvirtualhid issue #113](https://github.com/LizardByte/libvirtualhid/issues/113) |
|
| <a id="compatibility-note-14"></a><sup>14</sup> | Client platform and version | The marked features worked when tested with Moonlight on an iPhone running iOS 18.7.10, but did not work on an Apple TV 4K running tvOS 26.6. Moonlight enables these extended features only when Apple's Game Controller framework exposes the corresponding buttons, haptics localities, motion sensors, or light. | [Moonlight capability detection](https://github.com/moonlight-stream/moonlight-ios/blob/85af0f75622bb2636481afda8b0fc5cc33d5956e/Limelight/Input/ControllerSupport.m#L547-L608), [Apple controller-haptics capabilities](https://developer.apple.com/documentation/gamecontroller/gcdevicehaptics), and [Apple controller-motion capabilities](https://developer.apple.com/documentation/gamecontroller/gcmotion) |
|
||||||
| <a id="compatibility-note-15"></a><sup>15</sup> | Client platform and version | The marked features worked when tested with Moonlight on an iPhone running iOS 18.7.10, but did not work on an Apple TV 4K running tvOS 26.6. Moonlight enables these extended features only when Apple's Game Controller framework exposes the corresponding buttons, haptics localities, motion sensors, or light. | [Moonlight capability detection](https://github.com/moonlight-stream/moonlight-ios/blob/85af0f75622bb2636481afda8b0fc5cc33d5956e/Limelight/Input/ControllerSupport.m#L547-L608), [Apple controller-haptics capabilities](https://developer.apple.com/documentation/gamecontroller/gcdevicehaptics), and [Apple controller-motion capabilities](https://developer.apple.com/documentation/gamecontroller/gcmotion) |
|
| <a id="compatibility-note-15"></a><sup>15</sup> | Client capability and output pipeline | Both host backends decode Switch Pro Set HOME Light output as a grayscale LED callback. Moonlight Qt v6.1.0 uses SDL2's RGB-style LED capability check, and the tested controller reported no LED. Moonlight Qt master uses SDL3 through sdl2-compat; SDL3 identifies HOME as a mono LED, but the compatibility check maps only the RGB capability. Neither path advertises LED support to Sunshine, so it never sends the HOME-light command. | [Moonlight Qt LED capability check](https://github.com/moonlight-stream/moonlight-qt/blob/v6.1.0/app/streaming/input/gamepad.cpp), [SDL Switch HOME-light capability](https://github.com/libsdl-org/SDL/blob/147a8ee32dbf9ac02f3794964490687b6bbda1bc/src/joystick/hidapi/SDL_hidapi_switch.c), and [sdl2-compat LED mapping](https://github.com/libsdl-org/sdl2-compat/blob/a53b6ad90ecd2d0ccfe01d5cfd2059793acf8c12/src/sdl2_compat.c) |
|
||||||
|
|
||||||
Analog trigger input reports intermediate values between 0 and 1. Switch Pro
|
Analog trigger input reports intermediate values between 0 and 1. Switch Pro
|
||||||
ZL/ZR input is digital and reports only 0 or 1. Trigger input is also separate
|
ZL/ZR input is digital and reports only 0 or 1. Trigger input is also separate
|
||||||
@@ -228,7 +229,7 @@ and verify that the Sunshine virtual-gamepad selection matches a controller the
|
|||||||
game supports. Disconnect unused host-side controllers if the game always
|
game supports. Disconnect unused host-side controllers if the game always
|
||||||
opens the first controller slot.
|
opens the first controller slot.
|
||||||
|
|
||||||
### PlayStation gyro, light bar, or rumble does not work in Steam
|
### Gyro, LEDs, or rumble do not work in Steam
|
||||||
|
|
||||||
Steam may need a one-time gyro calibration before its controller tester or
|
Steam may need a one-time gyro calibration before its controller tester or
|
||||||
Steam Input fully initializes a virtual DualShock 4 or DualSense controller's
|
Steam Input fully initializes a virtual DualShock 4 or DualSense controller's
|
||||||
@@ -239,6 +240,12 @@ be a Steam bug rather than a remaining controller-protocol failure. See
|
|||||||
[ValveSoftware/steam-for-linux issue #13435](https://github.com/ValveSoftware/steam-for-linux/issues/13435)
|
[ValveSoftware/steam-for-linux issue #13435](https://github.com/ValveSoftware/steam-for-linux/issues/13435)
|
||||||
for a related DualSense rumble report.
|
for a related DualSense rumble report.
|
||||||
|
|
||||||
|
For Switch Pro, first confirm that rumble is enabled in the game and complete
|
||||||
|
Steam's controller setup or calibration once. A reported Linux test began
|
||||||
|
receiving rumble after the game-side rumble option was enabled; that observation
|
||||||
|
does not establish that every game or Steam configuration uses the same output
|
||||||
|
path.
|
||||||
|
|
||||||
If Steam repeatedly treats the virtual controller as a new device, disabling
|
If Steam repeatedly treats the virtual controller as a new device, disabling
|
||||||
Sunshine's **Randomize virtual controller MAC** option may help it retain the
|
Sunshine's **Randomize virtual controller MAC** option may help it retain the
|
||||||
controller's calibration and settings. Restart Sunshine and reconnect the
|
controller's calibration and settings. Restart Sunshine and reconnect the
|
||||||
|
|||||||
+30
-15
@@ -81,7 +81,15 @@ The VHF driver answers the calibration, pairing, and firmware feature reports
|
|||||||
used to initialize DualShock 4 and DualSense HIDAPI output. It also answers the
|
used to initialize DualShock 4 and DualSense HIDAPI output. It also answers the
|
||||||
Switch Pro USB and subcommand initialization sequence and accepts the native
|
Switch Pro USB and subcommand initialization sequence and accepts the native
|
||||||
`0x30` input layout, so descriptor-aware consumers can initialize those
|
`0x30` input layout, so descriptor-aware consumers can initialize those
|
||||||
controllers before sending their native output reports.
|
controllers before sending their native output reports. The Switch Pro profile
|
||||||
|
uses the `0x0210` hardware revision reported by a physical Nintendo controller,
|
||||||
|
and the Windows VHF device exposes that revision to HID consumers. Full-state
|
||||||
|
and subcommand-reply reports use the same per-device packet counter on Windows,
|
||||||
|
matching the counter that a native controller advances for every input report.
|
||||||
|
The Windows client backend caches the newest complete Switch Pro state and
|
||||||
|
streams native `0x30` reports every 15 milliseconds. This coalesces separate
|
||||||
|
acceleration and gyroscope API updates into the three-sample report cadence used
|
||||||
|
by a physical USB controller.
|
||||||
|
|
||||||
Windows VHF devices do not expose a Bluetooth transport identity to HIDAPI.
|
Windows VHF devices do not expose a Bluetooth transport identity to HIDAPI.
|
||||||
The Windows backend therefore reports DualShock 4 and DualSense requests as
|
The Windows backend therefore reports DualShock 4 and DualSense requests as
|
||||||
@@ -101,26 +109,23 @@ and signing details.
|
|||||||
|
|
||||||
The Linux backend uses standard user-space kernel interfaces:
|
The Linux backend uses standard user-space kernel interfaces:
|
||||||
|
|
||||||
- `uhid` for descriptor-driven HID gamepads.
|
- `uhid` for descriptor-driven PlayStation and Switch Pro gamepads.
|
||||||
- `uinput` for Generic, Xbox 360, Xbox One, Xbox Series, and Switch Pro
|
- `uinput` for Generic, Xbox 360, Xbox One, and Xbox Series gamepads, plus
|
||||||
gamepads, plus keyboard, mouse, touchscreen, trackpad, and pen tablet
|
keyboard, mouse, touchscreen, trackpad, and pen tablet devices.
|
||||||
devices.
|
|
||||||
- `libevdev` internally for uinput device construction.
|
- `libevdev` internally for uinput device construction.
|
||||||
- X11/XTest only as a keyboard and mouse fallback when `uinput` cannot be used
|
- X11/XTest only as a keyboard and mouse fallback when `uinput` cannot be used
|
||||||
and an X11 session is available.
|
and an X11 session is available.
|
||||||
|
|
||||||
Gamepad support normally prefers `uhid` because descriptors, raw HID identity,
|
Gamepad support normally prefers `uhid` because descriptors, raw HID identity,
|
||||||
feature reports, and output reports matter for controller compatibility.
|
feature reports, and output reports matter for controller compatibility.
|
||||||
Generic, Xbox-family, and Switch Pro profiles instead use `uinput` so SDL,
|
Generic and Xbox-family profiles instead use `uinput` so SDL,
|
||||||
Steam, browser Gamepad API implementations, and other evdev consumers receive
|
Steam, browser Gamepad API implementations, and other evdev consumers receive
|
||||||
canonical Linux gamepad events. Face buttons, shoulders, menu buttons, stick
|
canonical Linux gamepad events. Face buttons, shoulders, menu buttons, stick
|
||||||
clicks, and Guide use their native evdev codes; sticks use absolute axes. Every
|
clicks, and Guide use their native evdev codes; sticks use absolute axes. Every
|
||||||
uinput gamepad exposes its directional pad through `ABS_HAT0X` and `ABS_HAT0Y`.
|
uinput gamepad exposes its directional pad through `ABS_HAT0X` and `ABS_HAT0Y`.
|
||||||
Generic and Xbox triggers remain independent analog `ABS_Z` and `ABS_RZ` axes.
|
Generic and Xbox triggers remain independent analog `ABS_Z` and `ABS_RZ` axes.
|
||||||
Switch Pro uses the Nintendo face-button
|
Profiles with rumble support normalize rumble, constant, periodic, and ramp
|
||||||
positions, button events for ZL/ZR, and `BTN_Z` for Capture. Profiles with rumble
|
uinput force-feedback effects back into the public callback. Each requested playback repetition
|
||||||
support normalize rumble, constant, periodic, and ramp uinput force-feedback
|
|
||||||
effects back into the public callback. Each requested playback repetition
|
|
||||||
restarts the effect's ramp and envelope timing. A zero-length effect remains
|
restarts the effect's ramp and envelope timing. A zero-length effect remains
|
||||||
active until its explicit stop event, matching the infinite-effect contract used
|
active until its explicit stop event, matching the infinite-effect contract used
|
||||||
by SDL and Steam. The Linux backend lets a new uinput device settle before
|
by SDL and Steam. The Linux backend lets a new uinput device settle before
|
||||||
@@ -182,10 +187,16 @@ descriptor, and report framing remain unchanged. This transport-only name is
|
|||||||
confined to the Linux backend; public profile names, Windows names, and VHF
|
confined to the Linux backend; public profile names, Windows names, and VHF
|
||||||
behavior are unchanged.
|
behavior are unchanged.
|
||||||
|
|
||||||
Switch Pro keeps its Nintendo identity on the Linux uinput path. This follows
|
Switch Pro uses Linux `uhid` with its native Nintendo descriptor and identity.
|
||||||
the evdev layout used by Linux-native virtual-controller implementations and
|
Its backend-only UHID identity advertises Bluetooth transport because SDL2's
|
||||||
allows standard `FF_RUMBLE` effects without emulating the physical controller's
|
Linux HIDAPI rejects virtual `BUS_USB` HIDRAW devices without a physical USB
|
||||||
proprietary initialization handshake.
|
parent in sysfs. The public profile remains USB and its report framing is
|
||||||
|
unchanged. The backend answers Nintendo subcommand initialization reports, and
|
||||||
|
native `0x30` input reports carry buttons, sticks, battery state, and three live
|
||||||
|
IMU samples.
|
||||||
|
The public acceleration and gyroscope units remain meters per second squared
|
||||||
|
and degrees per second; the packer converts them to Nintendo's coordinate
|
||||||
|
system and sensor scales.
|
||||||
|
|
||||||
Linux touchscreen and trackpad contacts use the lowest available multitouch
|
Linux touchscreen and trackpad contacts use the lowest available multitouch
|
||||||
slot while they are active. A newly placed contact receives a new tracking ID,
|
slot while they are active. A newly placed contact receives a new tracking ID,
|
||||||
@@ -194,7 +205,11 @@ finger cannot overwrite another active finger in standard evdev consumers.
|
|||||||
|
|
||||||
On descriptor-driven backends, native Switch Pro output reports `0x01` and
|
On descriptor-driven backends, native Switch Pro output reports `0x01` and
|
||||||
`0x10` are decoded into the normalized low- and high-frequency rumble callback.
|
`0x10` are decoded into the normalized low- and high-frequency rumble callback.
|
||||||
The original native report remains available in `GamepadOutput::raw_report`.
|
Set Player Lights subcommand `0x30` additionally produces a `player_leds`
|
||||||
|
callback with separate solid and flashing states for the four indicators. The
|
||||||
|
Set HOME Light subcommand `0x38` produces a grayscale `rgb_led` callback whose
|
||||||
|
equal channels preserve the requested monochrome intensity. The original native
|
||||||
|
report remains available in `GamepadOutput::raw_report`.
|
||||||
|
|
||||||
The optional `virtualhid_control` diagnostic UI uses SDL3 and Dear ImGui through
|
The optional `virtualhid_control` diagnostic UI uses SDL3 and Dear ImGui through
|
||||||
the repository CPM lockfile. It is intended to stay on the same UI framework for
|
the repository CPM lockfile. It is intended to stay on the same UI framework for
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ A streaming host should be able to:
|
|||||||
indexes.
|
indexes.
|
||||||
- Submit incremental button, axis, trigger, touchpad, motion, and battery
|
- Submit incremental button, axis, trigger, touchpad, motion, and battery
|
||||||
updates without recreating a device.
|
updates without recreating a device.
|
||||||
- Receive output callbacks for rumble, LEDs, adaptive triggers, trigger rumble,
|
- Receive output callbacks for rumble, RGB and player LEDs, adaptive triggers,
|
||||||
and raw output reports where the selected profile supports them.
|
trigger rumble, and raw output reports where the selected profile supports
|
||||||
|
them.
|
||||||
- Query profile and backend capabilities before warning users about unsupported
|
- Query profile and backend capabilities before warning users about unsupported
|
||||||
client features.
|
client features.
|
||||||
- Read device nodes and platform paths when a downstream consumer or diagnostic
|
- Read device nodes and platform paths when a downstream consumer or diagnostic
|
||||||
@@ -51,8 +52,10 @@ The core API and adapter shape cover the major streaming-host requirements:
|
|||||||
- Rich controller metadata.
|
- Rich controller metadata.
|
||||||
- Gamepad output callbacks.
|
- Gamepad output callbacks.
|
||||||
- Keyboard and mouse input paths.
|
- Keyboard and mouse input paths.
|
||||||
- Linux PlayStation gamepads through `uhid`, Generic/Xbox/Switch Pro gamepads
|
- Linux PlayStation and Switch Pro gamepads through `uhid`, Generic/Xbox
|
||||||
through `uinput`, and `uinput` keyboard/pointer devices.
|
gamepads through `uinput`, and `uinput` keyboard/pointer devices.
|
||||||
|
- Native Switch Pro motion, initialization replies, rumble, HOME-light, and
|
||||||
|
player-light output handling on Linux and Windows descriptor-driven backends.
|
||||||
- Linux DualSense and DualShock 4 USB/Bluetooth report handling.
|
- Linux DualSense and DualShock 4 USB/Bluetooth report handling.
|
||||||
- Linux touchscreen, trackpad, and pen tablet device types.
|
- Linux touchscreen, trackpad, and pen tablet device types.
|
||||||
- FreeBSD uinput gamepads and pointer devices, with basic PlayStation input and
|
- FreeBSD uinput gamepads and pointer devices, with basic PlayStation input and
|
||||||
|
|||||||
+7
-6
@@ -105,8 +105,8 @@ default when a static target is available.
|
|||||||
The UI can create and remove gamepads from the built-in profiles, submit
|
The UI can create and remove gamepads from the built-in profiles, submit
|
||||||
buttons, sticks, triggers, and battery state, show backend and profile
|
buttons, sticks, triggers, and battery state, show backend and profile
|
||||||
capabilities, list device nodes reported for UI-created devices, and display
|
capabilities, list device nodes reported for UI-created devices, and display
|
||||||
normalized gamepad output such as rumble, RGB LED, adaptive trigger, trigger
|
normalized gamepad output such as rumble, RGB LED, player LED, adaptive trigger,
|
||||||
rumble, and raw report events delivered through the normal callback path. Button
|
trigger rumble, and raw report events delivered through the normal callback path. Button
|
||||||
controls are momentary by default, so they behave like physical gamepad buttons;
|
controls are momentary by default, so they behave like physical gamepad buttons;
|
||||||
on Windows, the UI also displays broker license status and can activate,
|
on Windows, the UI also displays broker license status and can activate,
|
||||||
refresh, or deactivate a machine license without elevation. Windows UMDF
|
refresh, or deactivate a machine license without elevation. Windows UMDF
|
||||||
@@ -229,10 +229,11 @@ through 16 in the input report. Linux may still route that profile through
|
|||||||
`uinput`, where the backend exposes those same logical directions through the
|
`uinput`, where the backend exposes those same logical directions through the
|
||||||
standard `ABS_HAT0X` and `ABS_HAT0Y` axes.
|
standard `ABS_HAT0X` and `ABS_HAT0Y` axes.
|
||||||
|
|
||||||
Profiles advertise support for features such as rumble, trigger rumble, RGB
|
Profiles advertise support for features such as rumble, trigger rumble, RGB and
|
||||||
LEDs, adaptive triggers, motion sensors, touchpads, battery state,
|
player LEDs, adaptive triggers, motion sensors, touchpads, battery state,
|
||||||
profile-specific buttons, and raw output reports. Consumers should query profile and
|
profile-specific buttons, and raw output reports. Consumers should query
|
||||||
backend capabilities before warning users about unsupported client features.
|
profile and backend capabilities before warning users about unsupported client
|
||||||
|
features.
|
||||||
The `misc1` button represents Share/Capture/Mic Mute-style controls and is
|
The `misc1` button represents Share/Capture/Mic Mute-style controls and is
|
||||||
available on the generic, Xbox Series, DualSense, and Switch Pro profiles; Xbox
|
available on the generic, Xbox Series, DualSense, and Switch Pro profiles; Xbox
|
||||||
360 and Xbox One do not advertise that extra button.
|
360 and Xbox One do not advertise that extra button.
|
||||||
@@ -427,7 +427,14 @@ gamepad.
|
|||||||
|
|
||||||
DualShock 4 and DualSense answer the calibration, pairing, and firmware feature
|
DualShock 4 and DualSense answer the calibration, pairing, and firmware feature
|
||||||
requests used by their Windows HIDAPI initialization paths. Switch Pro answers
|
requests used by their Windows HIDAPI initialization paths. Switch Pro answers
|
||||||
the native USB and subcommand handshake and submits native `0x30` input reports.
|
the native USB and subcommand handshake and submits native `0x30` input reports
|
||||||
|
with three live IMU samples. The client backend caches the newest complete
|
||||||
|
Switch state and submits it every 15 milliseconds, matching a physical USB
|
||||||
|
controller's report cadence while coalescing separate acceleration and
|
||||||
|
gyroscope updates. Its Set Player Lights subcommand is normalized into solid
|
||||||
|
and flashing player-indicator output states for the creating runtime, and its
|
||||||
|
monochrome HOME light is normalized as equal RGB channels so existing streaming
|
||||||
|
LED feedback paths can preserve its intensity.
|
||||||
The built-in Generic profile is presented to Windows as a DirectInput PID
|
The built-in Generic profile is presented to Windows as a DirectInput PID
|
||||||
Joystick with the complete output-report set required for DirectInput
|
Joystick with the complete output-report set required for DirectInput
|
||||||
enumeration. Constant Force and Sine output is normalized to the portable
|
enumeration. Constant Force and Sine output is normalized to the portable
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ namespace lvh {
|
|||||||
support.supports_rumble = profile.capabilities.supports_rumble;
|
support.supports_rumble = profile.capabilities.supports_rumble;
|
||||||
support.supports_rgb_led = profile.capabilities.supports_rgb_led;
|
support.supports_rgb_led = profile.capabilities.supports_rgb_led;
|
||||||
support.supports_adaptive_triggers = profile.capabilities.supports_adaptive_triggers;
|
support.supports_adaptive_triggers = profile.capabilities.supports_adaptive_triggers;
|
||||||
|
support.supports_player_leds = profile.capabilities.supports_player_leds;
|
||||||
support.supports_motion = profile.capabilities.supports_motion;
|
support.supports_motion = profile.capabilities.supports_motion;
|
||||||
support.supports_touchpad = profile.capabilities.supports_touchpad;
|
support.supports_touchpad = profile.capabilities.supports_touchpad;
|
||||||
support.supports_battery = profile.capabilities.supports_battery;
|
support.supports_battery = profile.capabilities.supports_battery;
|
||||||
@@ -142,6 +143,8 @@ namespace lvh {
|
|||||||
return support.supports_rgb_led;
|
return support.supports_rgb_led;
|
||||||
case adaptive_triggers:
|
case adaptive_triggers:
|
||||||
return support.supports_adaptive_triggers;
|
return support.supports_adaptive_triggers;
|
||||||
|
case player_leds:
|
||||||
|
return support.supports_player_leds;
|
||||||
case raw_report:
|
case raw_report:
|
||||||
return profile.output_report_size > 0U;
|
return profile.output_report_size > 0U;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2025,13 +2025,19 @@ namespace lvh::profiles {
|
|||||||
profile.bus_type = BusType::usb;
|
profile.bus_type = BusType::usb;
|
||||||
profile.vendor_id = 0x057E;
|
profile.vendor_id = 0x057E;
|
||||||
profile.product_id = 0x2009;
|
profile.product_id = 0x2009;
|
||||||
profile.version = 0x8111;
|
profile.version = 0x0210;
|
||||||
profile.report_id = switch_pro_report_id;
|
profile.report_id = switch_pro_report_id;
|
||||||
profile.input_report_size = switch_pro_input_report_size;
|
profile.input_report_size = switch_pro_input_report_size;
|
||||||
profile.output_report_size = switch_pro_output_report_size;
|
profile.output_report_size = switch_pro_output_report_size;
|
||||||
profile.name = "(libvirtualhid) Nintendo Pro Controller";
|
profile.name = "(libvirtualhid) Nintendo Pro Controller";
|
||||||
profile.manufacturer = "Nintendo Co., Ltd.";
|
profile.manufacturer = "Nintendo Co., Ltd.";
|
||||||
profile.capabilities = {.supports_rumble = true, .supports_motion = true, .supports_battery = true};
|
profile.capabilities = {
|
||||||
|
.supports_rumble = true,
|
||||||
|
.supports_motion = true,
|
||||||
|
.supports_rgb_led = true,
|
||||||
|
.supports_battery = true,
|
||||||
|
.supports_player_leds = true,
|
||||||
|
};
|
||||||
profile.report_descriptor = make_switch_pro_report_descriptor();
|
profile.report_descriptor = make_switch_pro_report_descriptor();
|
||||||
return profile;
|
return profile;
|
||||||
}
|
}
|
||||||
|
|||||||
+91
-7
@@ -18,6 +18,8 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
// local includes
|
// local includes
|
||||||
|
#include "shared/switch_pro_protocol.hpp"
|
||||||
|
|
||||||
#include <libvirtualhid/report.hpp>
|
#include <libvirtualhid/report.hpp>
|
||||||
|
|
||||||
namespace lvh::reports {
|
namespace lvh::reports {
|
||||||
@@ -75,6 +77,30 @@ namespace lvh::reports {
|
|||||||
|
|
||||||
constexpr std::size_t switch_rumble_output_report_size = 10;
|
constexpr std::size_t switch_rumble_output_report_size = 10;
|
||||||
|
|
||||||
|
constexpr std::uint8_t switch_set_player_lights_subcommand = 0x30;
|
||||||
|
|
||||||
|
constexpr std::uint8_t switch_set_home_light_subcommand = 0x38;
|
||||||
|
|
||||||
|
constexpr float switch_acceleration_scale = 4096.0F / 9.80665F;
|
||||||
|
|
||||||
|
constexpr float switch_gyroscope_scale = 14.2842F;
|
||||||
|
|
||||||
|
std::uint8_t decode_switch_home_light_intensity(std::byte encoded_intensity) {
|
||||||
|
const auto intensity = std::to_integer<std::uint8_t>(encoded_intensity >> 4U);
|
||||||
|
if (intensity == 0U) {
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
if (intensity <= 6U) {
|
||||||
|
return static_cast<std::uint8_t>(std::lround(static_cast<float>(intensity) * 25.5F));
|
||||||
|
}
|
||||||
|
if (intensity == 15U) {
|
||||||
|
return 255U;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto normalized = (static_cast<float>(intensity) - 0.5F) / 15.0F;
|
||||||
|
return static_cast<std::uint8_t>(std::lround(std::pow(normalized, 1.0F / 2.13F) * 255.0F));
|
||||||
|
}
|
||||||
|
|
||||||
// SDL maps 16-bit rumble strengths to Nintendo's shared 101-step amplitude
|
// SDL maps 16-bit rumble strengths to Nintendo's shared 101-step amplitude
|
||||||
// scale. This is the inverse table for the packed high- and low-band values:
|
// scale. This is the inverse table for the packed high- and low-band values:
|
||||||
// https://github.com/libsdl-org/SDL/blob/main/src/joystick/hidapi/SDL_hidapi_switch.c
|
// https://github.com/libsdl-org/SDL/blob/main/src/joystick/hidapi/SDL_hidapi_switch.c
|
||||||
@@ -950,6 +976,47 @@ namespace lvh::reports {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void append_switch_pro_outputs(
|
||||||
|
const std::vector<std::uint8_t> &report,
|
||||||
|
std::vector<GamepadOutput> &outputs
|
||||||
|
) {
|
||||||
|
if (const auto rumble = decode_switch_rumble_report(report); rumble.has_value()) {
|
||||||
|
GamepadOutput output;
|
||||||
|
output.kind = GamepadOutputKind::rumble;
|
||||||
|
output.low_frequency_rumble = rumble->low_frequency;
|
||||||
|
output.high_frequency_rumble = rumble->high_frequency;
|
||||||
|
output.raw_report = report;
|
||||||
|
outputs.push_back(std::move(output));
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
report.size() >= 12U && report[0] == switch_rumble_and_subcommand_output_report_id &&
|
||||||
|
report[10] == switch_set_player_lights_subcommand
|
||||||
|
) {
|
||||||
|
GamepadOutput output;
|
||||||
|
output.kind = GamepadOutputKind::player_leds;
|
||||||
|
const auto player_lights = std::byte {report[11]};
|
||||||
|
for (std::size_t index = 0; index < output.player_leds.size(); ++index) {
|
||||||
|
output.player_leds[index] = (player_lights & (std::byte {1} << index)) != zero_byte;
|
||||||
|
output.flashing_player_leds[index] =
|
||||||
|
(player_lights & (std::byte {1} << (index + 4U))) != zero_byte;
|
||||||
|
}
|
||||||
|
output.raw_report = report;
|
||||||
|
outputs.push_back(std::move(output));
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
report.size() >= 15U && report[0] == switch_rumble_and_subcommand_output_report_id &&
|
||||||
|
report[10] == switch_set_home_light_subcommand
|
||||||
|
) {
|
||||||
|
GamepadOutput output;
|
||||||
|
output.kind = GamepadOutputKind::rgb_led;
|
||||||
|
output.red = decode_switch_home_light_intensity(std::byte {report[12]});
|
||||||
|
output.green = output.red;
|
||||||
|
output.blue = output.red;
|
||||||
|
output.raw_report = report;
|
||||||
|
outputs.push_back(std::move(output));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
float clamp_axis(float value) {
|
float clamp_axis(float value) {
|
||||||
@@ -1061,6 +1128,22 @@ namespace lvh::reports {
|
|||||||
report[offset + 2U] = to_byte((y >> 4U) & 0xFFU);
|
report[offset + 2U] = to_byte((y >> 4U) & 0xFFU);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void write_switch_imu_sample(
|
||||||
|
ByteReport &report,
|
||||||
|
std::size_t offset,
|
||||||
|
const Vector3 &acceleration,
|
||||||
|
const Vector3 &gyroscope
|
||||||
|
) {
|
||||||
|
// Nintendo's native coordinate system differs from the portable
|
||||||
|
// PlayStation-style coordinate system exposed by GamepadState.
|
||||||
|
write_i16(report, offset, scale_i16(-acceleration.z, switch_acceleration_scale));
|
||||||
|
write_i16(report, offset + 2U, scale_i16(-acceleration.x, switch_acceleration_scale));
|
||||||
|
write_i16(report, offset + 4U, scale_i16(acceleration.y, switch_acceleration_scale));
|
||||||
|
write_i16(report, offset + 6U, scale_i16(-gyroscope.z, switch_gyroscope_scale));
|
||||||
|
write_i16(report, offset + 8U, scale_i16(-gyroscope.x, switch_gyroscope_scale));
|
||||||
|
write_i16(report, offset + 10U, scale_i16(gyroscope.y, switch_gyroscope_scale));
|
||||||
|
}
|
||||||
|
|
||||||
std::byte switch_battery_and_connection(const std::optional<GamepadBattery> &battery) {
|
std::byte switch_battery_and_connection(const std::optional<GamepadBattery> &battery) {
|
||||||
constexpr auto usb_connection = std::byte {0x01};
|
constexpr auto usb_connection = std::byte {0x01};
|
||||||
if (!battery.has_value()) {
|
if (!battery.has_value()) {
|
||||||
@@ -1151,6 +1234,7 @@ namespace lvh::reports {
|
|||||||
|
|
||||||
ByteReport report(profile.input_report_size, zero_byte);
|
ByteReport report(profile.input_report_size, zero_byte);
|
||||||
report[0] = to_byte(profile.report_id);
|
report[0] = to_byte(profile.report_id);
|
||||||
|
report[1] = to_byte(detail::switch_pro_protocol::next_switch_pro_packet_timer());
|
||||||
report[2] = switch_battery_and_connection(normalized.battery);
|
report[2] = switch_battery_and_connection(normalized.battery);
|
||||||
|
|
||||||
if (normalized.buttons.test(x)) {
|
if (normalized.buttons.test(x)) {
|
||||||
@@ -1222,6 +1306,11 @@ namespace lvh::reports {
|
|||||||
normalize_switch_stick_axis(normalized.right_stick.x),
|
normalize_switch_stick_axis(normalized.right_stick.x),
|
||||||
normalize_switch_stick_axis(normalized.right_stick.y)
|
normalize_switch_stick_axis(normalized.right_stick.y)
|
||||||
);
|
);
|
||||||
|
const auto acceleration = normalized.acceleration.value_or(Vector3 {.y = 9.80665F});
|
||||||
|
const auto gyroscope = normalized.gyroscope.value_or(Vector3 {});
|
||||||
|
for (const auto offset : {13U, 25U, 37U}) {
|
||||||
|
write_switch_imu_sample(report, offset, acceleration, gyroscope);
|
||||||
|
}
|
||||||
return to_uint8_report(report);
|
return to_uint8_report(report);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1302,13 +1391,8 @@ namespace lvh::reports {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (profile.gamepad_kind == GamepadProfileKind::switch_pro) {
|
if (profile.gamepad_kind == GamepadProfileKind::switch_pro) {
|
||||||
if (const auto rumble = decode_switch_rumble_report(report); rumble.has_value()) {
|
append_switch_pro_outputs(report, outputs);
|
||||||
GamepadOutput output;
|
if (!outputs.empty()) {
|
||||||
output.kind = GamepadOutputKind::rumble;
|
|
||||||
output.low_frequency_rumble = rumble->low_frequency;
|
|
||||||
output.high_frequency_rumble = rumble->high_frequency;
|
|
||||||
output.raw_report = report;
|
|
||||||
outputs.push_back(std::move(output));
|
|
||||||
return outputs;
|
return outputs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ namespace lvh {
|
|||||||
* @brief Number of rear paddle buttons exposed by the profile.
|
* @brief Number of rear paddle buttons exposed by the profile.
|
||||||
*/
|
*/
|
||||||
std::uint8_t supported_rear_paddle_count = 0;
|
std::uint8_t supported_rear_paddle_count = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the profile supports player indicator LED output.
|
||||||
|
*/
|
||||||
|
bool supports_player_leds = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -270,6 +270,11 @@ namespace lvh {
|
|||||||
* @brief Whether the profile supports adaptive trigger output.
|
* @brief Whether the profile supports adaptive trigger output.
|
||||||
*/
|
*/
|
||||||
bool supports_adaptive_triggers = false;
|
bool supports_adaptive_triggers = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the profile supports player indicator LED output.
|
||||||
|
*/
|
||||||
|
bool supports_player_leds = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -998,6 +1003,7 @@ namespace lvh {
|
|||||||
adaptive_triggers, ///< Adaptive trigger output.
|
adaptive_triggers, ///< Adaptive trigger output.
|
||||||
raw_report, ///< Raw output report bytes.
|
raw_report, ///< Raw output report bytes.
|
||||||
trigger_rumble, ///< Independent trigger rumble output.
|
trigger_rumble, ///< Independent trigger rumble output.
|
||||||
|
player_leds, ///< Player indicator LED output.
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1073,6 +1079,16 @@ namespace lvh {
|
|||||||
* @brief Raw output report payload.
|
* @brief Raw output report payload.
|
||||||
*/
|
*/
|
||||||
std::vector<std::uint8_t> raw_report;
|
std::vector<std::uint8_t> raw_report;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Solid player indicator LED states, ordered from player one through four.
|
||||||
|
*/
|
||||||
|
std::array<bool, 4> player_leds {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Flashing player indicator LED states, ordered from player one through four.
|
||||||
|
*/
|
||||||
|
std::array<bool, 4> flashing_player_leds {};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -65,6 +65,7 @@
|
|||||||
#include "core/backend.hpp"
|
#include "core/backend.hpp"
|
||||||
#if defined(__linux__)
|
#if defined(__linux__)
|
||||||
#include "shared/playstation_feature_reports.hpp"
|
#include "shared/playstation_feature_reports.hpp"
|
||||||
|
#include "shared/switch_pro_protocol.hpp"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <libvirtualhid/profiles.hpp>
|
#include <libvirtualhid/profiles.hpp>
|
||||||
@@ -248,8 +249,8 @@ namespace lvh::detail {
|
|||||||
case xbox_360:
|
case xbox_360:
|
||||||
case xbox_one:
|
case xbox_one:
|
||||||
case xbox_series:
|
case xbox_series:
|
||||||
case switch_pro:
|
|
||||||
return true;
|
return true;
|
||||||
|
case switch_pro:
|
||||||
case dualshock4:
|
case dualshock4:
|
||||||
case dualsense:
|
case dualsense:
|
||||||
#if defined(__FreeBSD__)
|
#if defined(__FreeBSD__)
|
||||||
@@ -301,8 +302,11 @@ namespace lvh::detail {
|
|||||||
|
|
||||||
#if defined(__linux__)
|
#if defined(__linux__)
|
||||||
std::uint16_t to_uhid_bus(const DeviceProfile &profile) {
|
std::uint16_t to_uhid_bus(const DeviceProfile &profile) {
|
||||||
|
// Linux SDL2 HIDAPI requires BUS_USB hidraw devices to have a physical USB
|
||||||
|
// parent in sysfs. UHID devices do not, so expose Switch Pro through the
|
||||||
|
// Bluetooth HID path that accepts descriptor-driven virtual devices.
|
||||||
if (profile.gamepad_kind == GamepadProfileKind::switch_pro) {
|
if (profile.gamepad_kind == GamepadProfileKind::switch_pro) {
|
||||||
return BUS_VIRTUAL;
|
return BUS_BLUETOOTH;
|
||||||
}
|
}
|
||||||
return to_uhid_bus(profile.bus_type);
|
return to_uhid_bus(profile.bus_type);
|
||||||
}
|
}
|
||||||
@@ -3057,9 +3061,20 @@ namespace lvh::detail {
|
|||||||
void dispatch_output_report(const __u8 *data, std::size_t report_size) {
|
void dispatch_output_report(const __u8 *data, std::size_t report_size) {
|
||||||
const auto size = std::min<std::size_t>(report_size, UHID_DATA_MAX);
|
const auto size = std::min<std::size_t>(report_size, UHID_DATA_MAX);
|
||||||
std::vector<std::uint8_t> report(data, data + size);
|
std::vector<std::uint8_t> report(data, data + size);
|
||||||
|
send_switch_pro_reply(report);
|
||||||
dispatch_output_report(report);
|
dispatch_output_report(report);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void send_switch_pro_reply(const std::vector<std::uint8_t> &report) {
|
||||||
|
if (profile_.gamepad_kind != GamepadProfileKind::switch_pro) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const auto reply = switch_pro_protocol::make_switch_pro_reply(report); reply.has_value()) {
|
||||||
|
static_cast<void>(write_input_report({reply->begin(), reply->end()}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void dispatch_set_report(std::uint8_t report_number, const __u8 *data, std::size_t report_size) {
|
void dispatch_set_report(std::uint8_t report_number, const __u8 *data, std::size_t report_size) {
|
||||||
const auto size = std::min<std::size_t>(report_size, UHID_DATA_MAX);
|
const auto size = std::min<std::size_t>(report_size, UHID_DATA_MAX);
|
||||||
std::vector<std::uint8_t> report(data, data + size);
|
std::vector<std::uint8_t> report(data, data + size);
|
||||||
@@ -3199,6 +3214,7 @@ namespace lvh::detail {
|
|||||||
effective_profile.capabilities.supports_rgb_led = false;
|
effective_profile.capabilities.supports_rgb_led = false;
|
||||||
effective_profile.capabilities.supports_battery = false;
|
effective_profile.capabilities.supports_battery = false;
|
||||||
effective_profile.capabilities.supports_adaptive_triggers = false;
|
effective_profile.capabilities.supports_adaptive_triggers = false;
|
||||||
|
effective_profile.capabilities.supports_player_leds = false;
|
||||||
return effective_profile;
|
return effective_profile;
|
||||||
#else
|
#else
|
||||||
static_cast<void>(requested_profile);
|
static_cast<void>(requested_profile);
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
#include "mouse_protocol.hpp"
|
#include "mouse_protocol.hpp"
|
||||||
#include "playstation_feature_protocol.hpp"
|
#include "playstation_feature_protocol.hpp"
|
||||||
#include "rotating_trace_log.hpp"
|
#include "rotating_trace_log.hpp"
|
||||||
#include "switch_pro_protocol.hpp"
|
#include "shared/switch_pro_protocol.hpp"
|
||||||
#include "unique_win32_handle.hpp"
|
#include "unique_win32_handle.hpp"
|
||||||
#include "vhf_input_report_queue.hpp"
|
#include "vhf_input_report_queue.hpp"
|
||||||
#include "windows_device_identity.hpp"
|
#include "windows_device_identity.hpp"
|
||||||
@@ -122,6 +122,7 @@ namespace {
|
|||||||
lvh::detail::windows::VhfInputReportQueue pending_input_reports;
|
lvh::detail::windows::VhfInputReportQueue pending_input_reports;
|
||||||
std::shared_ptr<std::vector<std::uint8_t>> in_flight_input_report;
|
std::shared_ptr<std::vector<std::uint8_t>> in_flight_input_report;
|
||||||
std::size_t active_input_submissions {};
|
std::size_t active_input_submissions {};
|
||||||
|
std::uint8_t switch_pro_packet_timer {};
|
||||||
bool vhf_ready_for_input_report {};
|
bool vhf_ready_for_input_report {};
|
||||||
bool shutting_down {};
|
bool shutting_down {};
|
||||||
};
|
};
|
||||||
@@ -254,6 +255,12 @@ namespace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto report = std::make_shared<std::vector<std::uint8_t>>(std::move(*pending));
|
auto report = std::make_shared<std::vector<std::uint8_t>>(std::move(*pending));
|
||||||
|
if (
|
||||||
|
record.request.gamepad_kind == LVH_WINDOWS_GAMEPAD_SWITCH_PRO && report->size() > 1U &&
|
||||||
|
(report->at(0) == 0x21U || report->at(0) == 0x30U)
|
||||||
|
) {
|
||||||
|
report->at(1) = record.switch_pro_packet_timer++;
|
||||||
|
}
|
||||||
const auto configured_report_id = record.request.hardware_ids.report_id;
|
const auto configured_report_id = record.request.hardware_ids.report_id;
|
||||||
const auto report_id = configured_report_id == 0U || report->empty() ? configured_report_id : report->front();
|
const auto report_id = configured_report_id == 0U || report->empty() ? configured_report_id : report->front();
|
||||||
|
|
||||||
@@ -871,7 +878,7 @@ namespace {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto reply = lvh::detail::windows::make_switch_pro_reply({event.report.data(), event.report_size});
|
auto reply = lvh::detail::switch_pro_protocol::make_switch_pro_reply({event.report.data(), event.report_size});
|
||||||
if (!reply.has_value()) {
|
if (!reply.has_value()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ namespace lvh::detail {
|
|||||||
using SendInputFunction = std::function<UINT(std::span<INPUT>)>;
|
using SendInputFunction = std::function<UINT(std::span<INPUT>)>;
|
||||||
using SyncThreadDesktopFunction = std::function<HDESK()>;
|
using SyncThreadDesktopFunction = std::function<HDESK()>;
|
||||||
|
|
||||||
|
constexpr auto switch_pro_report_interval = std::chrono::milliseconds {15};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Thread-local desktop identity used for SendInput retry decisions.
|
* @brief Thread-local desktop identity used for SendInput retry decisions.
|
||||||
*/
|
*/
|
||||||
@@ -956,13 +958,13 @@ namespace lvh::detail {
|
|||||||
OutputCallback output_callback;
|
OutputCallback output_callback;
|
||||||
bool uses_generic_pid = false;
|
bool uses_generic_pid = false;
|
||||||
windows::GenericPidRumbleState generic_pid_rumble;
|
windows::GenericPidRumbleState generic_pid_rumble;
|
||||||
|
std::vector<std::uint8_t> switch_pro_input_report;
|
||||||
};
|
};
|
||||||
|
|
||||||
class WindowsGamepad final: public BackendGamepad {
|
class WindowsGamepad final: public BackendGamepad {
|
||||||
public:
|
public:
|
||||||
WindowsGamepad(std::shared_ptr<WindowsBackendContext> context, std::shared_ptr<WindowsVhfDeviceState> state):
|
WindowsGamepad(std::shared_ptr<WindowsBackendContext> context, std::shared_ptr<WindowsVhfDeviceState> state);
|
||||||
context_ {std::move(context)},
|
~WindowsGamepad() override;
|
||||||
state_ {std::move(state)} {}
|
|
||||||
|
|
||||||
OperationStatus submit(
|
OperationStatus submit(
|
||||||
const GamepadState &state,
|
const GamepadState &state,
|
||||||
@@ -973,8 +975,14 @@ namespace lvh::detail {
|
|||||||
OperationStatus close() override;
|
OperationStatus close() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void stream_switch_pro_reports(std::stop_token stop_token);
|
||||||
|
void stop_switch_pro_report_stream();
|
||||||
|
|
||||||
std::shared_ptr<WindowsBackendContext> context_;
|
std::shared_ptr<WindowsBackendContext> context_;
|
||||||
std::shared_ptr<WindowsVhfDeviceState> state_;
|
std::shared_ptr<WindowsVhfDeviceState> state_;
|
||||||
|
std::condition_variable switch_pro_report_ready_;
|
||||||
|
std::mutex switch_pro_report_mutex_;
|
||||||
|
std::jthread switch_pro_report_thread_;
|
||||||
};
|
};
|
||||||
|
|
||||||
class WindowsBackendContext: public std::enable_shared_from_this<WindowsBackendContext> {
|
class WindowsBackendContext: public std::enable_shared_from_this<WindowsBackendContext> {
|
||||||
@@ -1294,6 +1302,66 @@ namespace lvh::detail {
|
|||||||
std::map<std::uint64_t, std::weak_ptr<WindowsVhfDeviceState>> devices_;
|
std::map<std::uint64_t, std::weak_ptr<WindowsVhfDeviceState>> devices_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
WindowsGamepad::WindowsGamepad(
|
||||||
|
std::shared_ptr<WindowsBackendContext> context,
|
||||||
|
std::shared_ptr<WindowsVhfDeviceState> state
|
||||||
|
):
|
||||||
|
context_ {std::move(context)},
|
||||||
|
state_ {std::move(state)} {
|
||||||
|
if (state_->profile.gamepad_kind == GamepadProfileKind::switch_pro) {
|
||||||
|
switch_pro_report_thread_ = std::jthread {[this](std::stop_token stop_token) {
|
||||||
|
stream_switch_pro_reports(stop_token);
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowsGamepad::~WindowsGamepad() {
|
||||||
|
stop_switch_pro_report_stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowsGamepad::stream_switch_pro_reports(std::stop_token stop_token) {
|
||||||
|
auto next_report = std::chrono::steady_clock::now() + switch_pro_report_interval;
|
||||||
|
std::unique_lock report_lock {switch_pro_report_mutex_};
|
||||||
|
while (!stop_token.stop_requested()) {
|
||||||
|
static_cast<void>(switch_pro_report_ready_.wait_until(report_lock, next_report, [&stop_token] {
|
||||||
|
return stop_token.stop_requested();
|
||||||
|
}));
|
||||||
|
if (stop_token.stop_requested()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
report_lock.unlock();
|
||||||
|
std::vector<std::uint8_t> report;
|
||||||
|
{
|
||||||
|
std::lock_guard state_lock {state_->mutex_};
|
||||||
|
if (!state_->open) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
report = state_->switch_pro_input_report;
|
||||||
|
}
|
||||||
|
if (!report.empty()) {
|
||||||
|
static_cast<void>(context_->submit_device_report(state_, report));
|
||||||
|
}
|
||||||
|
report_lock.lock();
|
||||||
|
|
||||||
|
next_report += switch_pro_report_interval;
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
if (next_report <= now) {
|
||||||
|
next_report = now + switch_pro_report_interval;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WindowsGamepad::stop_switch_pro_report_stream() {
|
||||||
|
if (!switch_pro_report_thread_.joinable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_pro_report_thread_.request_stop();
|
||||||
|
switch_pro_report_ready_.notify_all();
|
||||||
|
switch_pro_report_thread_.join();
|
||||||
|
}
|
||||||
|
|
||||||
OperationStatus WindowsGamepad::submit(
|
OperationStatus WindowsGamepad::submit(
|
||||||
const GamepadState & /*state*/,
|
const GamepadState & /*state*/,
|
||||||
const std::vector<std::uint8_t> &report
|
const std::vector<std::uint8_t> &report
|
||||||
@@ -1310,6 +1378,11 @@ namespace lvh::detail {
|
|||||||
return OperationStatus::failure(invalid_argument, "Windows gamepad input report exceeds protocol limit");
|
return OperationStatus::failure(invalid_argument, "Windows gamepad input report exceeds protocol limit");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state_->profile.gamepad_kind == GamepadProfileKind::switch_pro) {
|
||||||
|
state_->switch_pro_input_report = report;
|
||||||
|
return OperationStatus::success();
|
||||||
|
}
|
||||||
|
|
||||||
if (state_->uses_generic_pid) {
|
if (state_->uses_generic_pid) {
|
||||||
return context_->submit_device_report(state_, windows::make_generic_windows_input_report(report));
|
return context_->submit_device_report(state_, windows::make_generic_windows_input_report(report));
|
||||||
}
|
}
|
||||||
@@ -1333,6 +1406,7 @@ namespace lvh::detail {
|
|||||||
}
|
}
|
||||||
|
|
||||||
OperationStatus WindowsGamepad::close() {
|
OperationStatus WindowsGamepad::close() {
|
||||||
|
stop_switch_pro_report_stream();
|
||||||
return context_->close_device(state_);
|
return context_->close_device(state_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-6
@@ -1,22 +1,28 @@
|
|||||||
/**
|
/**
|
||||||
* @file src/platform/windows/shared/switch_pro_protocol.hpp
|
* @file src/shared/switch_pro_protocol.hpp
|
||||||
* @brief Nintendo Switch Pro initialization replies used by the Windows VHF driver.
|
* @brief Nintendo Switch Pro initialization replies shared by native HID backends.
|
||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
// standard includes
|
// standard includes
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <atomic>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <span>
|
#include <span>
|
||||||
|
|
||||||
namespace lvh::detail::windows {
|
namespace lvh::detail::switch_pro_protocol {
|
||||||
|
|
||||||
inline constexpr std::size_t switch_pro_report_size = 64U;
|
inline constexpr std::size_t switch_pro_report_size = 64U;
|
||||||
using SwitchProReport = std::array<std::uint8_t, switch_pro_report_size>;
|
using SwitchProReport = std::array<std::uint8_t, switch_pro_report_size>;
|
||||||
|
|
||||||
|
inline std::uint8_t next_switch_pro_packet_timer() {
|
||||||
|
static std::atomic_uint32_t packet_timer {0};
|
||||||
|
return static_cast<std::uint8_t>(packet_timer.fetch_add(1U, std::memory_order_relaxed));
|
||||||
|
}
|
||||||
|
|
||||||
namespace switch_pro_protocol_detail {
|
namespace switch_pro_protocol_detail {
|
||||||
|
|
||||||
inline constexpr std::array<std::uint8_t, 6> controller_mac {0x02, 0x00, 0x00, 0x00, 0x00, 0x01};
|
inline constexpr std::array<std::uint8_t, 6> controller_mac {0x02, 0x00, 0x00, 0x00, 0x00, 0x01};
|
||||||
@@ -149,7 +155,10 @@ namespace lvh::detail::windows {
|
|||||||
|
|
||||||
} // namespace switch_pro_protocol_detail
|
} // namespace switch_pro_protocol_detail
|
||||||
|
|
||||||
inline std::optional<SwitchProReport> make_switch_pro_reply(std::span<const std::uint8_t> output_report) {
|
inline std::optional<SwitchProReport> make_switch_pro_reply(
|
||||||
|
std::span<const std::uint8_t> output_report,
|
||||||
|
std::optional<std::uint8_t> packet_timer = std::nullopt
|
||||||
|
) {
|
||||||
if (output_report.empty()) {
|
if (output_report.empty()) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
@@ -184,7 +193,10 @@ namespace lvh::detail::windows {
|
|||||||
|
|
||||||
SwitchProReport reply {};
|
SwitchProReport reply {};
|
||||||
reply[0] = 0x21;
|
reply[0] = 0x21;
|
||||||
switch_pro_protocol_detail::set_neutral_controller_state(reply, output_report[1]);
|
switch_pro_protocol_detail::set_neutral_controller_state(
|
||||||
|
reply,
|
||||||
|
packet_timer.has_value() ? *packet_timer : next_switch_pro_packet_timer()
|
||||||
|
);
|
||||||
auto acknowledgement = std::uint8_t {0x80};
|
auto acknowledgement = std::uint8_t {0x80};
|
||||||
if (subcommand == 0x10U) {
|
if (subcommand == 0x10U) {
|
||||||
acknowledgement = 0x90;
|
acknowledgement = 0x90;
|
||||||
@@ -201,4 +213,4 @@ namespace lvh::detail::windows {
|
|||||||
return reply;
|
return reply;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace lvh::detail::windows
|
} // namespace lvh::detail::switch_pro_protocol
|
||||||
+108
-46
@@ -137,6 +137,101 @@ namespace lvh::detail::test {
|
|||||||
GamepadOutput last;
|
GamepadOutput last;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief DualSense observations from a socketpair-backed UHID test.
|
||||||
|
*/
|
||||||
|
struct LinuxUhidDualSenseObservation {
|
||||||
|
/**
|
||||||
|
* @brief Whether the peer observed a calibration reply.
|
||||||
|
*/
|
||||||
|
bool saw_calibration = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the peer observed a pairing reply.
|
||||||
|
*/
|
||||||
|
bool saw_pairing = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the peer observed a firmware reply.
|
||||||
|
*/
|
||||||
|
bool saw_firmware = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the peer observed a signed Bluetooth feature reply.
|
||||||
|
*/
|
||||||
|
bool saw_feature_crc = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether periodic Bluetooth reports preserved motion and advanced sensor metadata.
|
||||||
|
*/
|
||||||
|
bool saw_bluetooth_input_with_live_sensor_metadata = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief DualShock 4 observations from a socketpair-backed UHID test.
|
||||||
|
*/
|
||||||
|
struct LinuxUhidDualShock4Observation {
|
||||||
|
/**
|
||||||
|
* @brief Whether the peer observed a calibration reply.
|
||||||
|
*/
|
||||||
|
bool saw_calibration = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the peer observed a pairing reply.
|
||||||
|
*/
|
||||||
|
bool saw_pairing = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the peer observed a firmware reply.
|
||||||
|
*/
|
||||||
|
bool saw_firmware = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the peer observed a signed Bluetooth feature reply.
|
||||||
|
*/
|
||||||
|
bool saw_feature_crc = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the peer observed a Bluetooth-framed input report.
|
||||||
|
*/
|
||||||
|
bool saw_bluetooth_input = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the peer observed a USB-framed input report.
|
||||||
|
*/
|
||||||
|
bool saw_usb_input = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Switch Pro observations from a socketpair-backed UHID test.
|
||||||
|
*/
|
||||||
|
struct LinuxUhidSwitchProObservation {
|
||||||
|
/**
|
||||||
|
* @brief Whether the peer observed a subcommand acknowledgement input report.
|
||||||
|
*/
|
||||||
|
bool saw_subcommand_reply = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Packet timer carried by the observed subcommand reply.
|
||||||
|
*/
|
||||||
|
std::optional<std::uint8_t> subcommand_reply_packet_timer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the peer observed live IMU samples in a submitted input report.
|
||||||
|
*/
|
||||||
|
bool saw_motion_input = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Packet timer carried by the observed motion input report.
|
||||||
|
*/
|
||||||
|
std::optional<std::uint8_t> motion_input_packet_timer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Whether the output callback exposed player indicator LEDs.
|
||||||
|
*/
|
||||||
|
bool saw_player_leds = false;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Result from a socketpair-backed UHID lifecycle test.
|
* @brief Result from a socketpair-backed UHID lifecycle test.
|
||||||
*/
|
*/
|
||||||
@@ -172,59 +267,19 @@ namespace lvh::detail::test {
|
|||||||
bool saw_get_report_reply = false;
|
bool saw_get_report_reply = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Whether the peer observed a DualSense calibration reply.
|
* @brief DualSense protocol observations.
|
||||||
*/
|
*/
|
||||||
bool saw_dualsense_calibration = false;
|
LinuxUhidDualSenseObservation dualsense;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Whether the peer observed a DualShock 4 calibration reply.
|
* @brief DualShock 4 protocol observations.
|
||||||
*/
|
*/
|
||||||
bool saw_dualshock4_calibration = false;
|
LinuxUhidDualShock4Observation dualshock4;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Whether the peer observed a DualSense pairing reply.
|
* @brief Switch Pro protocol observations.
|
||||||
*/
|
*/
|
||||||
bool saw_dualsense_pairing = false;
|
LinuxUhidSwitchProObservation switch_pro;
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Whether the peer observed a DualShock 4 pairing reply.
|
|
||||||
*/
|
|
||||||
bool saw_dualshock4_pairing = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Whether the peer observed a DualSense firmware reply.
|
|
||||||
*/
|
|
||||||
bool saw_dualsense_firmware = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Whether the peer observed a DualShock 4 firmware reply.
|
|
||||||
*/
|
|
||||||
bool saw_dualshock4_firmware = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Whether the peer observed a signed Bluetooth DualSense feature reply.
|
|
||||||
*/
|
|
||||||
bool saw_dualsense_feature_crc = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Whether the peer observed a signed Bluetooth DualShock 4 feature reply.
|
|
||||||
*/
|
|
||||||
bool saw_dualshock4_feature_crc = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Whether periodic Bluetooth DualSense reports preserved motion and advanced sensor metadata.
|
|
||||||
*/
|
|
||||||
bool saw_dualsense_bluetooth_input_with_live_sensor_metadata = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Whether the peer observed a Bluetooth-framed DualShock 4 input report.
|
|
||||||
*/
|
|
||||||
bool saw_dualshock4_bluetooth_input = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Whether the peer observed a USB-framed DualShock 4 input report.
|
|
||||||
*/
|
|
||||||
bool saw_dualshock4_usb_input = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Whether the peer observed a set-report reply.
|
* @brief Whether the peer observed a set-report reply.
|
||||||
@@ -824,6 +879,13 @@ namespace lvh::detail::test {
|
|||||||
*/
|
*/
|
||||||
LinuxUhidRoundTripResult linux_uhid_socketpair_roundtrip();
|
LinuxUhidRoundTripResult linux_uhid_socketpair_roundtrip();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Exercise Switch Pro UHID input, output, and subcommand replies over a socketpair.
|
||||||
|
*
|
||||||
|
* @return Round-trip result with motion and player-light observations.
|
||||||
|
*/
|
||||||
|
LinuxUhidRoundTripResult linux_switch_pro_uhid_socketpair_reports();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Exercise DualSense UHID feature-report replies over a socketpair.
|
* @brief Exercise DualSense UHID feature-report replies over a socketpair.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -30,6 +30,15 @@ namespace lvh::detail::test {
|
|||||||
std::size_t destroy_requests = 0;
|
std::size_t destroy_requests = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct WindowsSwitchReportStreamResult {
|
||||||
|
OperationStatus create_status;
|
||||||
|
OperationStatus submit_status;
|
||||||
|
OperationStatus close_status;
|
||||||
|
bool repeated_report = false;
|
||||||
|
std::vector<std::uint8_t> expected_report;
|
||||||
|
std::vector<std::vector<std::uint8_t>> submitted_reports;
|
||||||
|
};
|
||||||
|
|
||||||
struct WindowsBackendFailureResult {
|
struct WindowsBackendFailureResult {
|
||||||
OperationStatus invalid_argument_status;
|
OperationStatus invalid_argument_status;
|
||||||
OperationStatus unsupported_profile_status;
|
OperationStatus unsupported_profile_status;
|
||||||
@@ -239,6 +248,7 @@ namespace lvh::detail::test {
|
|||||||
};
|
};
|
||||||
|
|
||||||
WindowsBackendLifecycleResult windows_backend_fake_channel_lifecycle();
|
WindowsBackendLifecycleResult windows_backend_fake_channel_lifecycle();
|
||||||
|
WindowsSwitchReportStreamResult windows_backend_switch_report_stream();
|
||||||
WindowsPlayStationTransportResult windows_backend_playstation_transport();
|
WindowsPlayStationTransportResult windows_backend_playstation_transport();
|
||||||
WindowsGenericPidOrderingResult windows_backend_generic_pid_callback_ordering();
|
WindowsGenericPidOrderingResult windows_backend_generic_pid_callback_ordering();
|
||||||
WindowsHidKeyboardResult windows_backend_hid_keyboard();
|
WindowsHidKeyboardResult windows_backend_hid_keyboard();
|
||||||
|
|||||||
+87
-14
@@ -1586,6 +1586,79 @@ namespace lvh::detail::test {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LinuxUhidRoundTripResult linux_switch_pro_uhid_socketpair_reports() {
|
||||||
|
LinuxUhidRoundTripResult result;
|
||||||
|
std::array<int, 2> descriptors {-1, -1};
|
||||||
|
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, descriptors.data()) != 0) {
|
||||||
|
result.create_status = system_error_status(ErrorCode::backend_failure, "failed to create socketpair", errno);
|
||||||
|
result.submit_status = result.create_status;
|
||||||
|
result.close_status = result.create_status;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateGamepadOptions options;
|
||||||
|
options.profile = profiles::switch_pro();
|
||||||
|
options.metadata.stable_id = "libvirtualhid-switch-pro-roundtrip";
|
||||||
|
|
||||||
|
UhidGamepad gamepad {descriptors[0]};
|
||||||
|
auto event = create_started_profile_uhid_gamepad(gamepad, 12, options, descriptors[1], BUS_BLUETOOTH, result);
|
||||||
|
result.creation.saw_create = result.creation.saw_create &&
|
||||||
|
event.u.create2.rd_size == options.profile.report_descriptor.size();
|
||||||
|
|
||||||
|
gamepad.set_output_callback([&result](const GamepadOutput &output) {
|
||||||
|
++result.output.callback_count;
|
||||||
|
result.output.last = output;
|
||||||
|
if (
|
||||||
|
output.kind == GamepadOutputKind::player_leds &&
|
||||||
|
output.player_leds == std::array {true, false, true, false} &&
|
||||||
|
output.flashing_player_leds == std::array {false, true, false, true}
|
||||||
|
) {
|
||||||
|
result.switch_pro.saw_player_leds = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
event = {};
|
||||||
|
event.type = UHID_OUTPUT;
|
||||||
|
event.u.output.rtype = UHID_OUTPUT_REPORT;
|
||||||
|
event.u.output.size = static_cast<__u16>(options.profile.output_report_size);
|
||||||
|
event.u.output.data[0] = 0x01;
|
||||||
|
event.u.output.data[1] = 0x07;
|
||||||
|
const std::array<std::uint8_t, 8> neutral_rumble {0x00, 0x01, 0x40, 0x40, 0x00, 0x01, 0x40, 0x40};
|
||||||
|
std::ranges::copy(neutral_rumble, event.u.output.data + 2U);
|
||||||
|
event.u.output.data[10] = 0x30;
|
||||||
|
event.u.output.data[11] = 0xA5;
|
||||||
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
||||||
|
if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) {
|
||||||
|
if (event.u.input2.size > 1U) {
|
||||||
|
result.switch_pro.subcommand_reply_packet_timer = static_cast<std::uint8_t>(event.u.input2.data[1]);
|
||||||
|
}
|
||||||
|
result.switch_pro.saw_subcommand_reply = event.u.input2.size == options.profile.input_report_size &&
|
||||||
|
event.u.input2.data[0] == 0x21 &&
|
||||||
|
event.u.input2.data[13] == 0x80 &&
|
||||||
|
event.u.input2.data[14] == 0x30;
|
||||||
|
}
|
||||||
|
|
||||||
|
GamepadState state;
|
||||||
|
state.buttons.set(GamepadButton::a);
|
||||||
|
state.acceleration = Vector3 {.x = 9.80665F, .y = 19.6133F, .z = -9.80665F};
|
||||||
|
state.gyroscope = Vector3 {.x = 1.0F, .y = 2.0F, .z = -3.0F};
|
||||||
|
const auto report = reports::pack_input_report(options.profile, state);
|
||||||
|
result.submit_status = gamepad.submit(state, report);
|
||||||
|
if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) {
|
||||||
|
if (event.u.input2.size > 1U) {
|
||||||
|
result.switch_pro.motion_input_packet_timer = static_cast<std::uint8_t>(event.u.input2.data[1]);
|
||||||
|
}
|
||||||
|
result.switch_pro.saw_motion_input = event.u.input2.size == report.size() &&
|
||||||
|
event.u.input2.data[0] == 0x30 &&
|
||||||
|
(event.u.input2.data[3] & 0x08U) != 0U &&
|
||||||
|
std::equal(report.begin() + 13, report.begin() + 49, event.u.input2.data + 13U);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.close_status = gamepad.close();
|
||||||
|
static_cast<void>(::close(descriptors[1]));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
LinuxUhidRoundTripResult linux_dualsense_uhid_socketpair_reports() {
|
LinuxUhidRoundTripResult linux_dualsense_uhid_socketpair_reports() {
|
||||||
LinuxUhidRoundTripResult result;
|
LinuxUhidRoundTripResult result;
|
||||||
std::array<int, 2> descriptors {-1, -1};
|
std::array<int, 2> descriptors {-1, -1};
|
||||||
@@ -1641,7 +1714,7 @@ namespace lvh::detail::test {
|
|||||||
event.u.get_report.rnum = 0x05;
|
event.u.get_report.rnum = 0x05;
|
||||||
static_cast<void>(write_uhid_event(descriptors[1], event));
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
||||||
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
||||||
result.saw_dualsense_calibration = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size == 41U &&
|
result.dualsense.saw_calibration = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size == 41U &&
|
||||||
event.u.get_report_reply.data[0] == 0x05;
|
event.u.get_report_reply.data[0] == 0x05;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1651,7 +1724,7 @@ namespace lvh::detail::test {
|
|||||||
event.u.get_report.rnum = 0x09;
|
event.u.get_report.rnum = 0x09;
|
||||||
static_cast<void>(write_uhid_event(descriptors[1], event));
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
||||||
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
||||||
result.saw_dualsense_pairing = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size > 7 &&
|
result.dualsense.saw_pairing = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size > 7 &&
|
||||||
event.u.get_report_reply.data[0] == 0x09 &&
|
event.u.get_report_reply.data[0] == 0x09 &&
|
||||||
event.u.get_report_reply.data[1] == 0x07 &&
|
event.u.get_report_reply.data[1] == 0x07 &&
|
||||||
event.u.get_report_reply.data[6] == 0x02;
|
event.u.get_report_reply.data[6] == 0x02;
|
||||||
@@ -1663,7 +1736,7 @@ namespace lvh::detail::test {
|
|||||||
event.u.get_report.rnum = 0x20;
|
event.u.get_report.rnum = 0x20;
|
||||||
static_cast<void>(write_uhid_event(descriptors[1], event));
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
||||||
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
||||||
result.saw_dualsense_firmware = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size > 0 &&
|
result.dualsense.saw_firmware = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size > 0 &&
|
||||||
event.u.get_report_reply.data[0] == 0x20;
|
event.u.get_report_reply.data[0] == 0x20;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1731,7 +1804,7 @@ namespace lvh::detail::test {
|
|||||||
const auto crc_offset = report_size - 4U;
|
const auto crc_offset = report_size - 4U;
|
||||||
const auto expected_crc = crc32(std::span<const std::uint8_t> {event.u.input2.data, crc_offset}, playstation_crc_seed(0xA1));
|
const auto expected_crc = crc32(std::span<const std::uint8_t> {event.u.input2.data, crc_offset}, playstation_crc_seed(0xA1));
|
||||||
const auto actual_crc = read_u32_le(event.u.input2.data + crc_offset);
|
const auto actual_crc = read_u32_le(event.u.input2.data + crc_offset);
|
||||||
result.saw_dualsense_bluetooth_input_with_live_sensor_metadata =
|
result.dualsense.saw_bluetooth_input_with_live_sensor_metadata =
|
||||||
first_input_report_valid && expected_crc == actual_crc && event.u.input2.data[8] != first_sequence &&
|
first_input_report_valid && expected_crc == actual_crc && event.u.input2.data[8] != first_sequence &&
|
||||||
read_u32_le(event.u.input2.data + 29U) != first_sensor_timestamp && std::equal(first_sensor_values.begin(), first_sensor_values.end(), event.u.input2.data + 17U);
|
read_u32_le(event.u.input2.data + 29U) != first_sensor_timestamp && std::equal(first_sensor_values.begin(), first_sensor_values.end(), event.u.input2.data + 17U);
|
||||||
}
|
}
|
||||||
@@ -1744,7 +1817,7 @@ namespace lvh::detail::test {
|
|||||||
static_cast<void>(write_uhid_event(descriptors[1], event));
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
||||||
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
||||||
const auto report_size = static_cast<std::size_t>(event.u.get_report_reply.size);
|
const auto report_size = static_cast<std::size_t>(event.u.get_report_reply.size);
|
||||||
result.saw_dualsense_pairing = event.u.get_report_reply.err == 0 && report_size > 7U &&
|
result.dualsense.saw_pairing = event.u.get_report_reply.err == 0 && report_size > 7U &&
|
||||||
event.u.get_report_reply.data[0] == 0x09 &&
|
event.u.get_report_reply.data[0] == 0x09 &&
|
||||||
event.u.get_report_reply.data[1] == 0x07 &&
|
event.u.get_report_reply.data[1] == 0x07 &&
|
||||||
event.u.get_report_reply.data[6] == 0x02;
|
event.u.get_report_reply.data[6] == 0x02;
|
||||||
@@ -1755,7 +1828,7 @@ namespace lvh::detail::test {
|
|||||||
playstation_crc_seed(playstation_feature_reports::playstation_feature_crc_seed)
|
playstation_crc_seed(playstation_feature_reports::playstation_feature_crc_seed)
|
||||||
);
|
);
|
||||||
const auto actual_crc = read_u32_le(event.u.get_report_reply.data + crc_offset);
|
const auto actual_crc = read_u32_le(event.u.get_report_reply.data + crc_offset);
|
||||||
result.saw_dualsense_feature_crc = expected_crc == actual_crc;
|
result.dualsense.saw_feature_crc = expected_crc == actual_crc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1785,7 +1858,7 @@ namespace lvh::detail::test {
|
|||||||
event.u.create2.rd_size == options.profile.report_descriptor.size();
|
event.u.create2.rd_size == options.profile.report_descriptor.size();
|
||||||
|
|
||||||
if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) {
|
if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) {
|
||||||
result.saw_dualshock4_usb_input =
|
result.dualshock4.saw_usb_input =
|
||||||
event.u.input2.size == options.profile.input_report_size && event.u.input2.data[0] == options.profile.report_id;
|
event.u.input2.size == options.profile.input_report_size && event.u.input2.data[0] == options.profile.report_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1816,7 +1889,7 @@ namespace lvh::detail::test {
|
|||||||
event.u.get_report.rnum = 0x02;
|
event.u.get_report.rnum = 0x02;
|
||||||
static_cast<void>(write_uhid_event(descriptors[1], event));
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
||||||
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
||||||
result.saw_dualshock4_calibration = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size == 37 &&
|
result.dualshock4.saw_calibration = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size == 37 &&
|
||||||
event.u.get_report_reply.data[0] == 0x02;
|
event.u.get_report_reply.data[0] == 0x02;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1826,7 +1899,7 @@ namespace lvh::detail::test {
|
|||||||
event.u.get_report.rnum = 0x12;
|
event.u.get_report.rnum = 0x12;
|
||||||
static_cast<void>(write_uhid_event(descriptors[1], event));
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
||||||
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
||||||
result.saw_dualshock4_pairing = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size == 16 &&
|
result.dualshock4.saw_pairing = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size == 16 &&
|
||||||
event.u.get_report_reply.data[0] == 0x12 &&
|
event.u.get_report_reply.data[0] == 0x12 &&
|
||||||
event.u.get_report_reply.data[1] == 0x07 &&
|
event.u.get_report_reply.data[1] == 0x07 &&
|
||||||
event.u.get_report_reply.data[6] == 0x02;
|
event.u.get_report_reply.data[6] == 0x02;
|
||||||
@@ -1838,7 +1911,7 @@ namespace lvh::detail::test {
|
|||||||
event.u.get_report.rnum = 0xA3;
|
event.u.get_report.rnum = 0xA3;
|
||||||
static_cast<void>(write_uhid_event(descriptors[1], event));
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
||||||
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
||||||
result.saw_dualshock4_firmware = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size == 49 &&
|
result.dualshock4.saw_firmware = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size == 49 &&
|
||||||
event.u.get_report_reply.data[0] == 0xA3;
|
event.u.get_report_reply.data[0] == 0xA3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1876,7 +1949,7 @@ namespace lvh::detail::test {
|
|||||||
const auto crc_offset = report_size - 4U;
|
const auto crc_offset = report_size - 4U;
|
||||||
const auto expected_crc = crc32(std::span<const std::uint8_t> {event.u.input2.data, crc_offset}, playstation_crc_seed(0xA1));
|
const auto expected_crc = crc32(std::span<const std::uint8_t> {event.u.input2.data, crc_offset}, playstation_crc_seed(0xA1));
|
||||||
const auto actual_crc = read_u32_le(event.u.input2.data + crc_offset);
|
const auto actual_crc = read_u32_le(event.u.input2.data + crc_offset);
|
||||||
result.saw_dualshock4_bluetooth_input = expected_crc == actual_crc;
|
result.dualshock4.saw_bluetooth_input = expected_crc == actual_crc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1887,7 +1960,7 @@ namespace lvh::detail::test {
|
|||||||
static_cast<void>(write_uhid_event(descriptors[1], event));
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
||||||
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
||||||
const auto report_size = static_cast<std::size_t>(event.u.get_report_reply.size);
|
const auto report_size = static_cast<std::size_t>(event.u.get_report_reply.size);
|
||||||
result.saw_dualshock4_calibration = event.u.get_report_reply.err == 0 && report_size == 41U &&
|
result.dualshock4.saw_calibration = event.u.get_report_reply.err == 0 && report_size == 41U &&
|
||||||
event.u.get_report_reply.data[0] == 0x05;
|
event.u.get_report_reply.data[0] == 0x05;
|
||||||
if (report_size >= 4U) {
|
if (report_size >= 4U) {
|
||||||
const auto crc_offset = report_size - 4U;
|
const auto crc_offset = report_size - 4U;
|
||||||
@@ -1896,7 +1969,7 @@ namespace lvh::detail::test {
|
|||||||
playstation_crc_seed(playstation_feature_reports::playstation_feature_crc_seed)
|
playstation_crc_seed(playstation_feature_reports::playstation_feature_crc_seed)
|
||||||
);
|
);
|
||||||
const auto actual_crc = read_u32_le(event.u.get_report_reply.data + crc_offset);
|
const auto actual_crc = read_u32_le(event.u.get_report_reply.data + crc_offset);
|
||||||
result.saw_dualshock4_feature_crc = expected_crc == actual_crc;
|
result.dualshock4.saw_feature_crc = expected_crc == actual_crc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1906,7 +1979,7 @@ namespace lvh::detail::test {
|
|||||||
event.u.get_report.rnum = 0x12;
|
event.u.get_report.rnum = 0x12;
|
||||||
static_cast<void>(write_uhid_event(descriptors[1], event));
|
static_cast<void>(write_uhid_event(descriptors[1], event));
|
||||||
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) {
|
||||||
result.saw_dualshock4_pairing = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size == 16 &&
|
result.dualshock4.saw_pairing = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size == 16 &&
|
||||||
event.u.get_report_reply.data[0] == 0x12 &&
|
event.u.get_report_reply.data[0] == 0x12 &&
|
||||||
event.u.get_report_reply.data[1] == 0x07 &&
|
event.u.get_report_reply.data[1] == 0x07 &&
|
||||||
event.u.get_report_reply.data[6] == 0x02;
|
event.u.get_report_reply.data[6] == 0x02;
|
||||||
|
|||||||
+23
@@ -528,6 +528,29 @@ namespace lvh::detail {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WindowsSwitchReportStreamResult windows_backend_switch_report_stream() {
|
||||||
|
WindowsSwitchReportStreamResult result;
|
||||||
|
auto command_state = std::make_shared<FakeWindowsControlChannelState>();
|
||||||
|
auto backend = make_fake_windows_backend(command_state, std::make_shared<FakeWindowsControlChannelState>());
|
||||||
|
|
||||||
|
CreateGamepadOptions options;
|
||||||
|
options.profile = profiles::switch_pro();
|
||||||
|
auto created = backend->create_gamepad(8, options);
|
||||||
|
result.create_status = created.status;
|
||||||
|
if (created) {
|
||||||
|
result.expected_report.assign(options.profile.input_report_size, 0x5AU);
|
||||||
|
result.expected_report[0] = 0x30U;
|
||||||
|
result.submit_status = created.gamepad->submit({}, result.expected_report);
|
||||||
|
result.repeated_report = wait_until([&command_state] {
|
||||||
|
return command_state->submit_report_count() >= 2U;
|
||||||
|
});
|
||||||
|
result.close_status = created.gamepad->close();
|
||||||
|
result.submitted_reports = command_state->submit_reports();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
WindowsHidMouseResult windows_backend_hid_mouse() {
|
WindowsHidMouseResult windows_backend_hid_mouse() {
|
||||||
using enum MouseEventKind;
|
using enum MouseEventKind;
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,9 @@ TEST(FreeBsdBackendTest, CreatesEveryGamepadWithTheExpectedPlayStationSubset) {
|
|||||||
EXPECT_FALSE(support.supports_battery);
|
EXPECT_FALSE(support.supports_battery);
|
||||||
EXPECT_FALSE(support.supports_rgb_led);
|
EXPECT_FALSE(support.supports_rgb_led);
|
||||||
EXPECT_FALSE(support.supports_adaptive_triggers);
|
EXPECT_FALSE(support.supports_adaptive_triggers);
|
||||||
|
EXPECT_FALSE(support.supports_player_leds);
|
||||||
EXPECT_EQ(effective_profile.output_report_size, 0U);
|
EXPECT_EQ(effective_profile.output_report_size, 0U);
|
||||||
|
EXPECT_FALSE(lvh::supports_gamepad_output(effective_profile, lvh::GamepadOutputKind::player_leds));
|
||||||
EXPECT_FALSE(lvh::supports_gamepad_output(effective_profile, lvh::GamepadOutputKind::raw_report));
|
EXPECT_FALSE(lvh::supports_gamepad_output(effective_profile, lvh::GamepadOutputKind::raw_report));
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -52,8 +52,10 @@ TEST(GamepadAdapterTest, ReportsProfileSupport) {
|
|||||||
|
|
||||||
const auto switch_pro_support = lvh::gamepad_profile_support(switch_pro);
|
const auto switch_pro_support = lvh::gamepad_profile_support(switch_pro);
|
||||||
EXPECT_TRUE(switch_pro_support.supports_rumble);
|
EXPECT_TRUE(switch_pro_support.supports_rumble);
|
||||||
|
EXPECT_TRUE(switch_pro_support.supports_rgb_led);
|
||||||
EXPECT_TRUE(switch_pro_support.supports_motion);
|
EXPECT_TRUE(switch_pro_support.supports_motion);
|
||||||
EXPECT_TRUE(switch_pro_support.supports_battery);
|
EXPECT_TRUE(switch_pro_support.supports_battery);
|
||||||
|
EXPECT_TRUE(switch_pro_support.supports_player_leds);
|
||||||
EXPECT_TRUE(switch_pro_support.supports_misc1_button);
|
EXPECT_TRUE(switch_pro_support.supports_misc1_button);
|
||||||
|
|
||||||
const auto keyboard_support = lvh::gamepad_profile_support(keyboard);
|
const auto keyboard_support = lvh::gamepad_profile_support(keyboard);
|
||||||
@@ -97,7 +99,10 @@ TEST(GamepadAdapterTest, ChecksButtonsAndOutputsByProfile) {
|
|||||||
EXPECT_TRUE(lvh::supports_gamepad_output(dualshock4, lvh::GamepadOutputKind::raw_report));
|
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::adaptive_triggers));
|
||||||
EXPECT_TRUE(lvh::supports_gamepad_output(switch_pro, lvh::GamepadOutputKind::rumble));
|
EXPECT_TRUE(lvh::supports_gamepad_output(switch_pro, lvh::GamepadOutputKind::rumble));
|
||||||
|
EXPECT_TRUE(lvh::supports_gamepad_output(switch_pro, lvh::GamepadOutputKind::rgb_led));
|
||||||
|
EXPECT_TRUE(lvh::supports_gamepad_output(switch_pro, lvh::GamepadOutputKind::player_leds));
|
||||||
EXPECT_TRUE(lvh::supports_gamepad_output(switch_pro, lvh::GamepadOutputKind::raw_report));
|
EXPECT_TRUE(lvh::supports_gamepad_output(switch_pro, lvh::GamepadOutputKind::raw_report));
|
||||||
|
EXPECT_FALSE(lvh::supports_gamepad_output(generic, lvh::GamepadOutputKind::player_leds));
|
||||||
EXPECT_TRUE(lvh::supports_gamepad_output(generic, lvh::GamepadOutputKind::raw_report));
|
EXPECT_TRUE(lvh::supports_gamepad_output(generic, lvh::GamepadOutputKind::raw_report));
|
||||||
EXPECT_FALSE(lvh::supports_gamepad_output(keyboard, lvh::GamepadOutputKind::rumble));
|
EXPECT_FALSE(lvh::supports_gamepad_output(keyboard, lvh::GamepadOutputKind::rumble));
|
||||||
EXPECT_FALSE(lvh::supports_gamepad_output(generic, static_cast<lvh::GamepadOutputKind>(255)));
|
EXPECT_FALSE(lvh::supports_gamepad_output(generic, static_cast<lvh::GamepadOutputKind>(255)));
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ TEST_F(LinuxBackendTest, TranslatesMouseButtonsAndBusTypes) {
|
|||||||
EXPECT_EQ(lvh::detail::test::linux_uhid_bus(lvh::BusType::usb), BUS_USB);
|
EXPECT_EQ(lvh::detail::test::linux_uhid_bus(lvh::BusType::usb), BUS_USB);
|
||||||
EXPECT_EQ(lvh::detail::test::linux_uhid_bus(lvh::BusType::bluetooth), BUS_BLUETOOTH);
|
EXPECT_EQ(lvh::detail::test::linux_uhid_bus(lvh::BusType::bluetooth), BUS_BLUETOOTH);
|
||||||
EXPECT_EQ(lvh::detail::test::linux_gamepad_uhid_bus(lvh::GamepadProfileKind::xbox_series), BUS_USB);
|
EXPECT_EQ(lvh::detail::test::linux_gamepad_uhid_bus(lvh::GamepadProfileKind::xbox_series), BUS_USB);
|
||||||
EXPECT_EQ(lvh::detail::test::linux_gamepad_uhid_bus(lvh::GamepadProfileKind::switch_pro), BUS_VIRTUAL);
|
EXPECT_EQ(lvh::detail::test::linux_gamepad_uhid_bus(lvh::GamepadProfileKind::switch_pro), BUS_BLUETOOTH);
|
||||||
EXPECT_EQ(lvh::detail::test::linux_uinput_bus(lvh::BusType::bluetooth), BUS_BLUETOOTH);
|
EXPECT_EQ(lvh::detail::test::linux_uinput_bus(lvh::BusType::bluetooth), BUS_BLUETOOTH);
|
||||||
|
|
||||||
EXPECT_EQ(lvh::detail::test::linux_pen_tool(lvh::PenToolType::pen), BTN_TOOL_PEN);
|
EXPECT_EQ(lvh::detail::test::linux_pen_tool(lvh::PenToolType::pen), BTN_TOOL_PEN);
|
||||||
@@ -365,26 +365,25 @@ TEST_F(LinuxBackendTest, PipeBackedUinputGamepadsUseCanonicalLinuxEvents) {
|
|||||||
struct ButtonCase {
|
struct ButtonCase {
|
||||||
lvh::GamepadButton button;
|
lvh::GamepadButton button;
|
||||||
std::uint16_t linux_code;
|
std::uint16_t linux_code;
|
||||||
std::uint16_t switch_code;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr std::array button_cases {
|
constexpr std::array button_cases {
|
||||||
ButtonCase {a, BTN_SOUTH, BTN_EAST},
|
ButtonCase {a, BTN_SOUTH},
|
||||||
ButtonCase {b, BTN_EAST, BTN_SOUTH},
|
ButtonCase {b, BTN_EAST},
|
||||||
ButtonCase {x, BTN_NORTH, BTN_NORTH},
|
ButtonCase {x, BTN_NORTH},
|
||||||
ButtonCase {y, BTN_WEST, BTN_WEST},
|
ButtonCase {y, BTN_WEST},
|
||||||
ButtonCase {left_shoulder, BTN_TL, BTN_TL},
|
ButtonCase {left_shoulder, BTN_TL},
|
||||||
ButtonCase {right_shoulder, BTN_TR, BTN_TR},
|
ButtonCase {right_shoulder, BTN_TR},
|
||||||
ButtonCase {back, BTN_SELECT, BTN_SELECT},
|
ButtonCase {back, BTN_SELECT},
|
||||||
ButtonCase {start, BTN_START, BTN_START},
|
ButtonCase {start, BTN_START},
|
||||||
ButtonCase {guide, BTN_MODE, BTN_MODE},
|
ButtonCase {guide, BTN_MODE},
|
||||||
ButtonCase {left_stick, BTN_THUMBL, BTN_THUMBL},
|
ButtonCase {left_stick, BTN_THUMBL},
|
||||||
ButtonCase {right_stick, BTN_THUMBR, BTN_THUMBR},
|
ButtonCase {right_stick, BTN_THUMBR},
|
||||||
};
|
};
|
||||||
constexpr std::array profile_kinds {generic, xbox_360, xbox_one, xbox_series, switch_pro};
|
constexpr std::array profile_kinds {generic, xbox_360, xbox_one, xbox_series};
|
||||||
|
|
||||||
for (const auto kind : profile_kinds) {
|
for (const auto kind : profile_kinds) {
|
||||||
for (const auto &[button, linux_code, switch_code] : button_cases) {
|
for (const auto &[button, linux_code] : button_cases) {
|
||||||
lvh::GamepadState state;
|
lvh::GamepadState state;
|
||||||
state.buttons.set(button);
|
state.buttons.set(button);
|
||||||
const auto result = lvh::detail::test::linux_uinput_gamepad_submit_pipe(kind, state);
|
const auto result = lvh::detail::test::linux_uinput_gamepad_submit_pipe(kind, state);
|
||||||
@@ -394,7 +393,7 @@ TEST_F(LinuxBackendTest, PipeBackedUinputGamepadsUseCanonicalLinuxEvents) {
|
|||||||
ASSERT_EQ(pressed_codes.size(), 1U)
|
ASSERT_EQ(pressed_codes.size(), 1U)
|
||||||
<< "profile " << static_cast<int>(std::to_underlying(kind)) << " logical button "
|
<< "profile " << static_cast<int>(std::to_underlying(kind)) << " logical button "
|
||||||
<< static_cast<int>(std::to_underlying(button));
|
<< static_cast<int>(std::to_underlying(button));
|
||||||
EXPECT_EQ(pressed_codes.front(), kind == switch_pro ? switch_code : linux_code)
|
EXPECT_EQ(pressed_codes.front(), linux_code)
|
||||||
<< "profile " << static_cast<int>(std::to_underlying(kind)) << " logical button "
|
<< "profile " << static_cast<int>(std::to_underlying(kind)) << " logical button "
|
||||||
<< static_cast<int>(std::to_underlying(button));
|
<< static_cast<int>(std::to_underlying(button));
|
||||||
}
|
}
|
||||||
@@ -411,17 +410,6 @@ TEST_F(LinuxBackendTest, PipeBackedUinputGamepadsUseCanonicalLinuxEvents) {
|
|||||||
EXPECT_NE(pressed, result.events.end());
|
EXPECT_NE(pressed, result.events.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
lvh::GamepadState state;
|
|
||||||
state.buttons.set(misc1);
|
|
||||||
const auto result = lvh::detail::test::linux_uinput_gamepad_submit_pipe(switch_pro, state);
|
|
||||||
ASSERT_TRUE(result.status.ok()) << result.status.message();
|
|
||||||
const auto pressed = std::ranges::find_if(result.events, [](const auto &event) {
|
|
||||||
return event.type == EV_KEY && event.code == BTN_Z && event.value == 1;
|
|
||||||
});
|
|
||||||
EXPECT_NE(pressed, result.events.end());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto kind : profile_kinds) {
|
for (const auto kind : profile_kinds) {
|
||||||
lvh::GamepadState state;
|
lvh::GamepadState state;
|
||||||
state.buttons.set(dpad_up);
|
state.buttons.set(dpad_up);
|
||||||
@@ -451,15 +439,8 @@ TEST_F(LinuxBackendTest, PipeBackedUinputGamepadsUseCanonicalLinuxEvents) {
|
|||||||
EXPECT_EQ(event_value(EV_ABS, ABS_Y), lvh::reports::normalize_axis(-0.25F));
|
EXPECT_EQ(event_value(EV_ABS, ABS_Y), lvh::reports::normalize_axis(-0.25F));
|
||||||
EXPECT_EQ(event_value(EV_ABS, ABS_RX), lvh::reports::normalize_axis(0.75F));
|
EXPECT_EQ(event_value(EV_ABS, ABS_RX), lvh::reports::normalize_axis(0.75F));
|
||||||
EXPECT_EQ(event_value(EV_ABS, ABS_RY), lvh::reports::normalize_axis(1.0F));
|
EXPECT_EQ(event_value(EV_ABS, ABS_RY), lvh::reports::normalize_axis(1.0F));
|
||||||
if (kind == switch_pro) {
|
EXPECT_EQ(event_value(EV_ABS, ABS_Z), lvh::reports::normalize_trigger(0.25F));
|
||||||
EXPECT_EQ(event_value(EV_ABS, ABS_Z), std::nullopt);
|
EXPECT_EQ(event_value(EV_ABS, ABS_RZ), lvh::reports::normalize_trigger(0.75F));
|
||||||
EXPECT_EQ(event_value(EV_ABS, ABS_RZ), std::nullopt);
|
|
||||||
EXPECT_EQ(event_value(EV_KEY, BTN_TL2), 1);
|
|
||||||
EXPECT_EQ(event_value(EV_KEY, BTN_TR2), 1);
|
|
||||||
} else {
|
|
||||||
EXPECT_EQ(event_value(EV_ABS, ABS_Z), lvh::reports::normalize_trigger(0.25F));
|
|
||||||
EXPECT_EQ(event_value(EV_ABS, ABS_RZ), lvh::reports::normalize_trigger(0.75F));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,7 +452,6 @@ TEST_F(LinuxBackendTest, UinputGamepadsNormalizeForceFeedback) {
|
|||||||
xbox_360,
|
xbox_360,
|
||||||
xbox_one,
|
xbox_one,
|
||||||
xbox_series,
|
xbox_series,
|
||||||
switch_pro,
|
|
||||||
}) {
|
}) {
|
||||||
for (const auto effect_type : {FF_RUMBLE, FF_CONSTANT, FF_PERIODIC, FF_RAMP}) {
|
for (const auto effect_type : {FF_RUMBLE, FF_CONSTANT, FF_PERIODIC, FF_RAMP}) {
|
||||||
const auto result = lvh::detail::test::linux_uinput_gamepad_fake_rumble(kind, effect_type);
|
const auto result = lvh::detail::test::linux_uinput_gamepad_fake_rumble(kind, effect_type);
|
||||||
@@ -492,7 +472,7 @@ TEST_F(LinuxBackendTest, UinputGamepadsNormalizeForceFeedback) {
|
|||||||
|
|
||||||
TEST_F(LinuxBackendTest, UinputGamepadKeepsInfiniteRumbleActiveUntilExplicitStop) {
|
TEST_F(LinuxBackendTest, UinputGamepadKeepsInfiniteRumbleActiveUntilExplicitStop) {
|
||||||
const auto result = lvh::detail::test::linux_uinput_gamepad_fake_rumble(
|
const auto result = lvh::detail::test::linux_uinput_gamepad_fake_rumble(
|
||||||
lvh::GamepadProfileKind::switch_pro,
|
lvh::GamepadProfileKind::xbox_series,
|
||||||
FF_RUMBLE,
|
FF_RUMBLE,
|
||||||
0,
|
0,
|
||||||
true
|
true
|
||||||
@@ -509,7 +489,7 @@ TEST_F(LinuxBackendTest, UinputGamepadKeepsInfiniteRumbleActiveUntilExplicitStop
|
|||||||
|
|
||||||
TEST_F(LinuxBackendTest, UinputGamepadRecalculatesActiveRumbleEndAfterReupload) {
|
TEST_F(LinuxBackendTest, UinputGamepadRecalculatesActiveRumbleEndAfterReupload) {
|
||||||
const auto result = lvh::detail::test::linux_uinput_gamepad_fake_rumble(
|
const auto result = lvh::detail::test::linux_uinput_gamepad_fake_rumble(
|
||||||
lvh::GamepadProfileKind::switch_pro,
|
lvh::GamepadProfileKind::xbox_series,
|
||||||
FF_RUMBLE,
|
FF_RUMBLE,
|
||||||
0,
|
0,
|
||||||
false,
|
false,
|
||||||
@@ -802,6 +782,27 @@ TEST_F(LinuxBackendTest, SocketpairBackedUhidGamepadRoundTripsEvents) {
|
|||||||
EXPECT_EQ(result.output.last.high_frequency_rumble, 0x1234);
|
EXPECT_EQ(result.output.last.high_frequency_rumble, 0x1234);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(LinuxBackendTest, SocketpairBackedSwitchProUsesNativeUhidProtocol) {
|
||||||
|
const auto result = lvh::detail::test::linux_switch_pro_uhid_socketpair_reports();
|
||||||
|
EXPECT_TRUE(result.create_status.ok()) << result.create_status.message();
|
||||||
|
EXPECT_TRUE(result.submit_status.ok()) << result.submit_status.message();
|
||||||
|
EXPECT_TRUE(result.close_status.ok()) << result.close_status.message();
|
||||||
|
EXPECT_TRUE(result.creation.saw_create);
|
||||||
|
EXPECT_TRUE(result.creation.waited_for_start);
|
||||||
|
EXPECT_EQ(result.creation.name, lvh::profiles::switch_pro().name);
|
||||||
|
EXPECT_TRUE(result.switch_pro.saw_subcommand_reply);
|
||||||
|
EXPECT_TRUE(result.switch_pro.saw_motion_input);
|
||||||
|
ASSERT_TRUE(result.switch_pro.subcommand_reply_packet_timer.has_value());
|
||||||
|
ASSERT_TRUE(result.switch_pro.motion_input_packet_timer.has_value());
|
||||||
|
EXPECT_EQ(
|
||||||
|
*result.switch_pro.motion_input_packet_timer,
|
||||||
|
static_cast<std::uint8_t>(*result.switch_pro.subcommand_reply_packet_timer + 1U)
|
||||||
|
);
|
||||||
|
EXPECT_TRUE(result.switch_pro.saw_player_leds);
|
||||||
|
ASSERT_EQ(result.output.callback_count, 2U);
|
||||||
|
EXPECT_EQ(result.output.last.kind, lvh::GamepadOutputKind::player_leds);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(LinuxBackendTest, SocketpairBackedDualSenseRepliesToFeatureReports) {
|
TEST_F(LinuxBackendTest, SocketpairBackedDualSenseRepliesToFeatureReports) {
|
||||||
const auto result = lvh::detail::test::linux_dualsense_uhid_socketpair_reports();
|
const auto result = lvh::detail::test::linux_dualsense_uhid_socketpair_reports();
|
||||||
EXPECT_TRUE(result.create_status.ok()) << result.create_status.message();
|
EXPECT_TRUE(result.create_status.ok()) << result.create_status.message();
|
||||||
@@ -809,9 +810,9 @@ TEST_F(LinuxBackendTest, SocketpairBackedDualSenseRepliesToFeatureReports) {
|
|||||||
EXPECT_TRUE(result.creation.saw_create);
|
EXPECT_TRUE(result.creation.saw_create);
|
||||||
EXPECT_TRUE(result.creation.waited_for_start);
|
EXPECT_TRUE(result.creation.waited_for_start);
|
||||||
EXPECT_EQ(result.creation.name, "Wireless Controller");
|
EXPECT_EQ(result.creation.name, "Wireless Controller");
|
||||||
EXPECT_TRUE(result.saw_dualsense_calibration);
|
EXPECT_TRUE(result.dualsense.saw_calibration);
|
||||||
EXPECT_TRUE(result.saw_dualsense_pairing);
|
EXPECT_TRUE(result.dualsense.saw_pairing);
|
||||||
EXPECT_TRUE(result.saw_dualsense_firmware);
|
EXPECT_TRUE(result.dualsense.saw_firmware);
|
||||||
EXPECT_TRUE(result.saw_set_report_reply);
|
EXPECT_TRUE(result.saw_set_report_reply);
|
||||||
ASSERT_GE(result.output.callback_count, 1U);
|
ASSERT_GE(result.output.callback_count, 1U);
|
||||||
EXPECT_EQ(result.output.last.kind, lvh::GamepadOutputKind::rumble);
|
EXPECT_EQ(result.output.last.kind, lvh::GamepadOutputKind::rumble);
|
||||||
@@ -827,9 +828,9 @@ TEST_F(LinuxBackendTest, SocketpairBackedDualSenseBluetoothFramesReports) {
|
|||||||
EXPECT_TRUE(result.creation.saw_create);
|
EXPECT_TRUE(result.creation.saw_create);
|
||||||
EXPECT_TRUE(result.creation.waited_for_start);
|
EXPECT_TRUE(result.creation.waited_for_start);
|
||||||
EXPECT_EQ(result.creation.name, "Wireless Controller");
|
EXPECT_EQ(result.creation.name, "Wireless Controller");
|
||||||
EXPECT_TRUE(result.saw_dualsense_bluetooth_input_with_live_sensor_metadata);
|
EXPECT_TRUE(result.dualsense.saw_bluetooth_input_with_live_sensor_metadata);
|
||||||
EXPECT_TRUE(result.saw_dualsense_pairing);
|
EXPECT_TRUE(result.dualsense.saw_pairing);
|
||||||
EXPECT_TRUE(result.saw_dualsense_feature_crc);
|
EXPECT_TRUE(result.dualsense.saw_feature_crc);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(LinuxBackendTest, SocketpairBackedDualShock4RepliesToFeatureReports) {
|
TEST_F(LinuxBackendTest, SocketpairBackedDualShock4RepliesToFeatureReports) {
|
||||||
@@ -839,10 +840,10 @@ TEST_F(LinuxBackendTest, SocketpairBackedDualShock4RepliesToFeatureReports) {
|
|||||||
EXPECT_TRUE(result.creation.saw_create);
|
EXPECT_TRUE(result.creation.saw_create);
|
||||||
EXPECT_TRUE(result.creation.waited_for_start);
|
EXPECT_TRUE(result.creation.waited_for_start);
|
||||||
EXPECT_EQ(result.creation.name, "Wireless Controller");
|
EXPECT_EQ(result.creation.name, "Wireless Controller");
|
||||||
EXPECT_TRUE(result.saw_dualshock4_usb_input);
|
EXPECT_TRUE(result.dualshock4.saw_usb_input);
|
||||||
EXPECT_TRUE(result.saw_dualshock4_calibration);
|
EXPECT_TRUE(result.dualshock4.saw_calibration);
|
||||||
EXPECT_TRUE(result.saw_dualshock4_pairing);
|
EXPECT_TRUE(result.dualshock4.saw_pairing);
|
||||||
EXPECT_TRUE(result.saw_dualshock4_firmware);
|
EXPECT_TRUE(result.dualshock4.saw_firmware);
|
||||||
EXPECT_TRUE(result.saw_set_report_reply);
|
EXPECT_TRUE(result.saw_set_report_reply);
|
||||||
ASSERT_GE(result.output.callback_count, 1U);
|
ASSERT_GE(result.output.callback_count, 1U);
|
||||||
EXPECT_EQ(result.output.last.kind, lvh::GamepadOutputKind::rumble);
|
EXPECT_EQ(result.output.last.kind, lvh::GamepadOutputKind::rumble);
|
||||||
@@ -857,10 +858,10 @@ TEST_F(LinuxBackendTest, SocketpairBackedDualShock4BluetoothFramesReports) {
|
|||||||
EXPECT_TRUE(result.creation.saw_create);
|
EXPECT_TRUE(result.creation.saw_create);
|
||||||
EXPECT_TRUE(result.creation.waited_for_start);
|
EXPECT_TRUE(result.creation.waited_for_start);
|
||||||
EXPECT_EQ(result.creation.name, "Wireless Controller");
|
EXPECT_EQ(result.creation.name, "Wireless Controller");
|
||||||
EXPECT_TRUE(result.saw_dualshock4_bluetooth_input);
|
EXPECT_TRUE(result.dualshock4.saw_bluetooth_input);
|
||||||
EXPECT_TRUE(result.saw_dualshock4_calibration);
|
EXPECT_TRUE(result.dualshock4.saw_calibration);
|
||||||
EXPECT_TRUE(result.saw_dualshock4_pairing);
|
EXPECT_TRUE(result.dualshock4.saw_pairing);
|
||||||
EXPECT_TRUE(result.saw_dualshock4_feature_crc);
|
EXPECT_TRUE(result.dualshock4.saw_feature_crc);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(LinuxBackendTest, FakeLinuxBackendCreatesAllDeviceTypes) {
|
TEST_F(LinuxBackendTest, FakeLinuxBackendCreatesAllDeviceTypes) {
|
||||||
@@ -984,15 +985,13 @@ TEST_F(LinuxBackendTest, FakeUinputConstructionCoversCapabilitiesAndFailureBranc
|
|||||||
std::uint16_t product_id;
|
std::uint16_t product_id;
|
||||||
bool key_record;
|
bool key_record;
|
||||||
bool sparse_button_slots;
|
bool sparse_button_slots;
|
||||||
bool switch_controls;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr std::array gamepad_cases {
|
constexpr std::array gamepad_cases {
|
||||||
GamepadCase {generic, BUS_USB, 0x1209, 0x0001, true, false, false},
|
GamepadCase {generic, BUS_USB, 0x1209, 0x0001, true, false},
|
||||||
GamepadCase {xbox_360, BUS_BLUETOOTH, 0x045E, 0x028E, false, true, false},
|
GamepadCase {xbox_360, BUS_BLUETOOTH, 0x045E, 0x028E, false, true},
|
||||||
GamepadCase {xbox_one, BUS_BLUETOOTH, 0x045E, 0x0B20, false, true, false},
|
GamepadCase {xbox_one, BUS_BLUETOOTH, 0x045E, 0x0B20, false, true},
|
||||||
GamepadCase {xbox_series, BUS_BLUETOOTH, 0x045E, 0x0B13, true, true, false},
|
GamepadCase {xbox_series, BUS_BLUETOOTH, 0x045E, 0x0B13, true, true},
|
||||||
GamepadCase {switch_pro, BUS_USB, 0x057E, 0x2009, false, false, true},
|
|
||||||
};
|
};
|
||||||
constexpr std::array active_buttons {
|
constexpr std::array active_buttons {
|
||||||
BTN_SOUTH,
|
BTN_SOUTH,
|
||||||
@@ -1012,7 +1011,7 @@ TEST_F(LinuxBackendTest, FakeUinputConstructionCoversCapabilitiesAndFailureBranc
|
|||||||
|
|
||||||
constexpr std::array feedback_codes {FF_RUMBLE, FF_CONSTANT, FF_PERIODIC, FF_SINE, FF_RAMP, FF_GAIN};
|
constexpr std::array feedback_codes {FF_RUMBLE, FF_CONSTANT, FF_PERIODIC, FF_SINE, FF_RAMP, FF_GAIN};
|
||||||
|
|
||||||
for (const auto &[kind, bustype, vendor_id, product_id, key_record, sparse_button_slots, switch_controls] : gamepad_cases) {
|
for (const auto &[kind, bustype, vendor_id, product_id, key_record, sparse_button_slots] : gamepad_cases) {
|
||||||
const auto expected_profile = lvh::profiles::gamepad_profile(kind);
|
const auto expected_profile = lvh::profiles::gamepad_profile(kind);
|
||||||
ASSERT_TRUE(expected_profile.has_value());
|
ASSERT_TRUE(expected_profile.has_value());
|
||||||
const auto gamepad = lvh::detail::test::linux_uinput_create_fake_gamepad(kind);
|
const auto gamepad = lvh::detail::test::linux_uinput_create_fake_gamepad(kind);
|
||||||
@@ -1029,7 +1028,7 @@ TEST_F(LinuxBackendTest, FakeUinputConstructionCoversCapabilitiesAndFailureBranc
|
|||||||
EXPECT_NE(find_code(gamepad, EV_KEY, button), nullptr) << "missing canonical gamepad button " << button;
|
EXPECT_NE(find_code(gamepad, EV_KEY, button), nullptr) << "missing canonical gamepad button " << button;
|
||||||
}
|
}
|
||||||
for (const auto button : reserved_buttons) {
|
for (const auto button : reserved_buttons) {
|
||||||
const auto expected = sparse_button_slots || (switch_controls && button != BTN_C);
|
const auto expected = sparse_button_slots;
|
||||||
EXPECT_EQ(find_code(gamepad, EV_KEY, button) != nullptr, expected)
|
EXPECT_EQ(find_code(gamepad, EV_KEY, button) != nullptr, expected)
|
||||||
<< "unexpected reserved gamepad button slot state for " << button;
|
<< "unexpected reserved gamepad button slot state for " << button;
|
||||||
}
|
}
|
||||||
@@ -1041,16 +1040,9 @@ TEST_F(LinuxBackendTest, FakeUinputConstructionCoversCapabilitiesAndFailureBranc
|
|||||||
EXPECT_NE(find_code(gamepad, EV_ABS, ABS_HAT0Y), nullptr);
|
EXPECT_NE(find_code(gamepad, EV_ABS, ABS_HAT0Y), nullptr);
|
||||||
EXPECT_EQ(find_code(gamepad, EV_KEY, KEY_RECORD) != nullptr, key_record);
|
EXPECT_EQ(find_code(gamepad, EV_KEY, KEY_RECORD) != nullptr, key_record);
|
||||||
const auto *left_trigger = find_code(gamepad, EV_ABS, ABS_Z);
|
const auto *left_trigger = find_code(gamepad, EV_ABS, ABS_Z);
|
||||||
if (switch_controls) {
|
ASSERT_NE(left_trigger, nullptr);
|
||||||
EXPECT_EQ(left_trigger, nullptr);
|
EXPECT_EQ(left_trigger->minimum, 0);
|
||||||
EXPECT_NE(find_code(gamepad, EV_KEY, BTN_TL2), nullptr);
|
EXPECT_EQ(left_trigger->maximum, 255);
|
||||||
EXPECT_NE(find_code(gamepad, EV_KEY, BTN_TR2), nullptr);
|
|
||||||
EXPECT_NE(find_code(gamepad, EV_KEY, BTN_Z), nullptr);
|
|
||||||
} else {
|
|
||||||
ASSERT_NE(left_trigger, nullptr);
|
|
||||||
EXPECT_EQ(left_trigger->minimum, 0);
|
|
||||||
EXPECT_EQ(left_trigger->maximum, 255);
|
|
||||||
}
|
|
||||||
for (const auto code : feedback_codes) {
|
for (const auto code : feedback_codes) {
|
||||||
EXPECT_EQ(find_code(gamepad, EV_FF, code) != nullptr, expected_profile->capabilities.supports_rumble);
|
EXPECT_EQ(find_code(gamepad, EV_FF, code) != nullptr, expected_profile->capabilities.supports_rumble);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ namespace {
|
|||||||
int minimum_buttons = 1;
|
int minimum_buttons = 1;
|
||||||
int minimum_axes = 2;
|
int minimum_axes = 2;
|
||||||
bool require_sdl_rumble = false;
|
bool require_sdl_rumble = false;
|
||||||
|
bool require_motion = false;
|
||||||
bool expect_live_input = true;
|
bool expect_live_input = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -335,7 +336,7 @@ namespace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int wait_for_sdl_joystick(const lvh::DeviceProfile &profile, bool require_rumble) {
|
int wait_for_sdl_game_controller(const lvh::DeviceProfile &profile, bool require_rumble) {
|
||||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds {3};
|
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds {3};
|
||||||
|
|
||||||
while (std::chrono::steady_clock::now() < deadline) {
|
while (std::chrono::steady_clock::now() < deadline) {
|
||||||
@@ -345,6 +346,7 @@ namespace {
|
|||||||
for (int index = 0; index < joystick_count; ++index) {
|
for (int index = 0; index < joystick_count; ++index) {
|
||||||
if (
|
if (
|
||||||
sdl_joystick_matches_profile(index, profile) &&
|
sdl_joystick_matches_profile(index, profile) &&
|
||||||
|
SDL_IsGameController(index) == SDL_TRUE &&
|
||||||
(!require_rumble || sdl_joystick_supports_rumble(index))
|
(!require_rumble || sdl_joystick_supports_rumble(index))
|
||||||
) {
|
) {
|
||||||
return index;
|
return index;
|
||||||
@@ -479,6 +481,7 @@ namespace {
|
|||||||
SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS4_RUMBLE", "1");
|
SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS4_RUMBLE", "1");
|
||||||
SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS5", "1");
|
SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS5", "1");
|
||||||
SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS5_RUMBLE", "1");
|
SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS5_RUMBLE", "1");
|
||||||
|
SDL_SetHint("SDL_JOYSTICK_HIDAPI_SWITCH", "1");
|
||||||
}
|
}
|
||||||
|
|
||||||
lvh::GamepadCreationResult create_sdl_gamepad(lvh::Runtime &runtime, const SdlGamepadConsumerCase &test_case) {
|
lvh::GamepadCreationResult create_sdl_gamepad(lvh::Runtime &runtime, const SdlGamepadConsumerCase &test_case) {
|
||||||
@@ -520,7 +523,25 @@ namespace {
|
|||||||
auto created = create_sdl_gamepad(*runtime, test_case);
|
auto created = create_sdl_gamepad(*runtime, test_case);
|
||||||
ASSERT_TRUE(created) << created.status.message();
|
ASSERT_TRUE(created) << created.status.message();
|
||||||
|
|
||||||
const auto joystick_index = wait_for_sdl_joystick(expected_profile, test_case.require_sdl_rumble);
|
if (test_case.require_sdl_rumble) {
|
||||||
|
const auto hidraw_node = wait_for_hidraw_node(*created.gamepad);
|
||||||
|
ASSERT_TRUE(hidraw_node.has_value())
|
||||||
|
<< "No hidraw node was discovered. Reported device nodes: "
|
||||||
|
<< describe_device_nodes(created.gamepad->device_nodes());
|
||||||
|
|
||||||
|
const auto access_error = wait_for_write_access(*hidraw_node);
|
||||||
|
ASSERT_EQ(access_error, 0)
|
||||||
|
<< "hidraw node is not writable: " << errno_message(access_error)
|
||||||
|
<< " (errno=" << access_error << "); " << describe_node_permissions(*hidraw_node);
|
||||||
|
errno = 0;
|
||||||
|
const auto read_result = ::access(hidraw_node->c_str(), R_OK);
|
||||||
|
const auto read_error = errno;
|
||||||
|
ASSERT_EQ(read_result, 0)
|
||||||
|
<< "hidraw node is not readable: " << errno_message(read_error)
|
||||||
|
<< " (errno=" << read_error << "); " << describe_node_permissions(*hidraw_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto joystick_index = wait_for_sdl_game_controller(expected_profile, test_case.require_sdl_rumble);
|
||||||
ASSERT_GE(joystick_index, 0);
|
ASSERT_GE(joystick_index, 0);
|
||||||
|
|
||||||
test_body(expected_profile, joystick_index, *created.gamepad);
|
test_body(expected_profile, joystick_index, *created.gamepad);
|
||||||
@@ -550,6 +571,41 @@ namespace {
|
|||||||
EXPECT_GT(rumble->high_frequency.load(), 0);
|
EXPECT_GT(rumble->high_frequency.load(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void expect_sdl_motion_input(SDL_GameController *controller, lvh::Gamepad &gamepad) {
|
||||||
|
ASSERT_EQ(SDL_GameControllerHasSensor(controller, SDL_SENSOR_ACCEL), SDL_TRUE);
|
||||||
|
ASSERT_EQ(SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO), SDL_TRUE);
|
||||||
|
ASSERT_EQ(SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_TRUE), 0) << SDL_GetError();
|
||||||
|
ASSERT_EQ(SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE), 0) << SDL_GetError();
|
||||||
|
|
||||||
|
lvh::GamepadState state;
|
||||||
|
state.acceleration = lvh::Vector3 {.x = 9.80665F, .y = 19.6133F, .z = -9.80665F};
|
||||||
|
state.gyroscope = lvh::Vector3 {.x = 10.0F, .y = 20.0F, .z = -30.0F};
|
||||||
|
ASSERT_TRUE(gamepad.submit(state).ok());
|
||||||
|
|
||||||
|
std::array<float, 3> acceleration {};
|
||||||
|
std::array<float, 3> gyroscope {};
|
||||||
|
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds {3};
|
||||||
|
while (std::chrono::steady_clock::now() < deadline) {
|
||||||
|
SDL_GameControllerUpdate();
|
||||||
|
pump_sdl_events();
|
||||||
|
ASSERT_EQ(
|
||||||
|
SDL_GameControllerGetSensorData(controller, SDL_SENSOR_ACCEL, acceleration.data(), acceleration.size()),
|
||||||
|
0
|
||||||
|
) << SDL_GetError();
|
||||||
|
ASSERT_EQ(
|
||||||
|
SDL_GameControllerGetSensorData(controller, SDL_SENSOR_GYRO, gyroscope.data(), gyroscope.size()),
|
||||||
|
0
|
||||||
|
) << SDL_GetError();
|
||||||
|
if (std::abs(acceleration[1]) > 15.0F && std::abs(gyroscope[2]) > 0.25F) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds {20});
|
||||||
|
}
|
||||||
|
|
||||||
|
ADD_FAILURE() << "SDL motion remained static: accel=" << acceleration[0] << "," << acceleration[1] << ","
|
||||||
|
<< acceleration[2] << " gyro=" << gyroscope[0] << "," << gyroscope[1] << "," << gyroscope[2];
|
||||||
|
}
|
||||||
|
|
||||||
void expect_hidraw_rumble_callback(const lvh::DeviceProfile &profile, lvh::Gamepad &gamepad) {
|
void expect_hidraw_rumble_callback(const lvh::DeviceProfile &profile, lvh::Gamepad &gamepad) {
|
||||||
const auto rumble = observe_rumble(gamepad);
|
const auto rumble = observe_rumble(gamepad);
|
||||||
const auto hidraw_node = wait_for_hidraw_node(gamepad);
|
const auto hidraw_node = wait_for_hidraw_node(gamepad);
|
||||||
@@ -713,6 +769,9 @@ namespace {
|
|||||||
}
|
}
|
||||||
EXPECT_GT(SDL_GameControllerGetAxis(controller.get(), SDL_CONTROLLER_AXIS_TRIGGERLEFT), 0);
|
EXPECT_GT(SDL_GameControllerGetAxis(controller.get(), SDL_CONTROLLER_AXIS_TRIGGERLEFT), 0);
|
||||||
EXPECT_GT(SDL_GameControllerGetAxis(controller.get(), SDL_CONTROLLER_AXIS_TRIGGERRIGHT), 16000);
|
EXPECT_GT(SDL_GameControllerGetAxis(controller.get(), SDL_CONTROLLER_AXIS_TRIGGERRIGHT), 16000);
|
||||||
|
if (test_case.require_motion) {
|
||||||
|
expect_sdl_motion_input(controller.get(), gamepad);
|
||||||
|
}
|
||||||
expect_sdl_rumble_callback(controller.get(), gamepad);
|
expect_sdl_rumble_callback(controller.get(), gamepad);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -851,14 +910,16 @@ TEST_F(LinuxConsumerTest, SdlSeesXboxSeriesCanonicalButtons) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(LinuxConsumerTest, SdlSeesSwitchProCanonicalButtons) {
|
TEST_F(LinuxConsumerTest, SdlSeesSwitchProCanonicalButtons) {
|
||||||
ASSERT_TRUE(HasReadableWritableDeviceNode("/dev/uinput"));
|
ASSERT_TRUE(HasReadableWritableDeviceNode("/dev/uhid"));
|
||||||
|
|
||||||
run_sdl_canonical_gamepad_test({
|
run_sdl_canonical_gamepad_test({
|
||||||
.profile = lvh::profiles::switch_pro(),
|
.profile = lvh::profiles::switch_pro(),
|
||||||
.name_suffix = "SDL Switch Pro",
|
.name_suffix = "SDL Switch Pro",
|
||||||
.stable_id = "libvirtualhid-sdl-switch-pro-test",
|
.stable_id = "02:00:00:00:00:05",
|
||||||
.minimum_buttons = 14,
|
.minimum_buttons = 14,
|
||||||
.minimum_axes = 4,
|
.minimum_axes = 4,
|
||||||
|
.require_sdl_rumble = true,
|
||||||
|
.require_motion = true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -234,12 +234,14 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) {
|
|||||||
|
|
||||||
EXPECT_EQ(switch_pro.vendor_id, 0x057E);
|
EXPECT_EQ(switch_pro.vendor_id, 0x057E);
|
||||||
EXPECT_EQ(switch_pro.product_id, 0x2009);
|
EXPECT_EQ(switch_pro.product_id, 0x2009);
|
||||||
|
EXPECT_EQ(switch_pro.version, 0x0210);
|
||||||
EXPECT_EQ(switch_pro.manufacturer, "Nintendo Co., Ltd.");
|
EXPECT_EQ(switch_pro.manufacturer, "Nintendo Co., Ltd.");
|
||||||
EXPECT_EQ(switch_pro.report_id, 0x30);
|
EXPECT_EQ(switch_pro.report_id, 0x30);
|
||||||
EXPECT_EQ(switch_pro.input_report_size, 64U);
|
EXPECT_EQ(switch_pro.input_report_size, 64U);
|
||||||
EXPECT_EQ(switch_pro.output_report_size, 64U);
|
EXPECT_EQ(switch_pro.output_report_size, 64U);
|
||||||
EXPECT_TRUE(switch_pro.capabilities.supports_rumble);
|
EXPECT_TRUE(switch_pro.capabilities.supports_rumble);
|
||||||
EXPECT_TRUE(switch_pro.capabilities.supports_motion);
|
EXPECT_TRUE(switch_pro.capabilities.supports_motion);
|
||||||
|
EXPECT_TRUE(switch_pro.capabilities.supports_rgb_led);
|
||||||
EXPECT_TRUE(switch_pro.capabilities.supports_battery);
|
EXPECT_TRUE(switch_pro.capabilities.supports_battery);
|
||||||
|
|
||||||
const auto generic = lvh::profiles::generic_gamepad();
|
const auto generic = lvh::profiles::generic_gamepad();
|
||||||
|
|||||||
+194
-1
@@ -180,7 +180,6 @@ TEST(ReportTest, PacksSwitchProReport) {
|
|||||||
|
|
||||||
ASSERT_EQ(report.size(), profile.input_report_size);
|
ASSERT_EQ(report.size(), profile.input_report_size);
|
||||||
EXPECT_EQ(report[0], 0x30U);
|
EXPECT_EQ(report[0], 0x30U);
|
||||||
EXPECT_EQ(report[1], 0x00U); // Packet timer.
|
|
||||||
EXPECT_EQ(report[2], 0x81U); // Full battery and USB connection when battery state is unknown.
|
EXPECT_EQ(report[2], 0x81U); // Full battery and USB connection when battery state is unknown.
|
||||||
EXPECT_EQ(report[3], 0x0CU); // B and A.
|
EXPECT_EQ(report[3], 0x0CU); // B and A.
|
||||||
EXPECT_EQ(report[4], 0x32U); // Plus, Home, and Capture.
|
EXPECT_EQ(report[4], 0x32U); // Plus, Home, and Capture.
|
||||||
@@ -193,6 +192,48 @@ TEST(ReportTest, PacksSwitchProReport) {
|
|||||||
EXPECT_EQ(report[9], 0xFFU);
|
EXPECT_EQ(report[9], 0xFFU);
|
||||||
EXPECT_EQ(report[10], 0x0BU);
|
EXPECT_EQ(report[10], 0x0BU);
|
||||||
EXPECT_EQ(report[11], 0x40U);
|
EXPECT_EQ(report[11], 0x40U);
|
||||||
|
|
||||||
|
// Each full report carries three native IMU samples. A missing sensor value
|
||||||
|
// represents a stationary controller under one g of gravity.
|
||||||
|
for (const auto offset : {13U, 25U, 37U}) {
|
||||||
|
EXPECT_EQ(read_i16_le(report, offset), 0);
|
||||||
|
EXPECT_EQ(read_i16_le(report, offset + 2U), 0);
|
||||||
|
EXPECT_EQ(read_i16_le(report, offset + 4U), 4096);
|
||||||
|
EXPECT_EQ(read_i16_le(report, offset + 6U), 0);
|
||||||
|
EXPECT_EQ(read_i16_le(report, offset + 8U), 0);
|
||||||
|
EXPECT_EQ(read_i16_le(report, offset + 10U), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ReportTest, AdvancesSwitchProPacketTimerForEachInputReport) {
|
||||||
|
const auto profile = lvh::profiles::switch_pro();
|
||||||
|
|
||||||
|
const auto first = lvh::reports::pack_input_report(profile, {});
|
||||||
|
const auto second = lvh::reports::pack_input_report(profile, {});
|
||||||
|
|
||||||
|
ASSERT_EQ(first.size(), profile.input_report_size);
|
||||||
|
ASSERT_EQ(second.size(), profile.input_report_size);
|
||||||
|
EXPECT_EQ(second[1], static_cast<std::uint8_t>(first[1] + 1U));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ReportTest, PacksSwitchProMotionInEveryImuSample) {
|
||||||
|
const auto profile = lvh::profiles::switch_pro();
|
||||||
|
|
||||||
|
lvh::GamepadState state;
|
||||||
|
state.acceleration = lvh::Vector3 {.x = 9.80665F, .y = 19.6133F, .z = -9.80665F};
|
||||||
|
state.gyroscope = lvh::Vector3 {.x = 1.0F, .y = 2.0F, .z = -3.0F};
|
||||||
|
|
||||||
|
const auto report = lvh::reports::pack_input_report(profile, state);
|
||||||
|
|
||||||
|
ASSERT_EQ(report.size(), profile.input_report_size);
|
||||||
|
for (const auto offset : {13U, 25U, 37U}) {
|
||||||
|
EXPECT_EQ(read_i16_le(report, offset), 4096);
|
||||||
|
EXPECT_EQ(read_i16_le(report, offset + 2U), -4096);
|
||||||
|
EXPECT_EQ(read_i16_le(report, offset + 4U), 8192);
|
||||||
|
EXPECT_EQ(read_i16_le(report, offset + 6U), 43);
|
||||||
|
EXPECT_EQ(read_i16_le(report, offset + 8U), -14);
|
||||||
|
EXPECT_EQ(read_i16_le(report, offset + 10U), 29);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(ReportTest, PacksXboxGipNeutralReport) {
|
TEST(ReportTest, PacksXboxGipNeutralReport) {
|
||||||
@@ -665,6 +706,158 @@ TEST(ReportTest, ParsesSwitchProRumbleFromSubcommandReport) {
|
|||||||
EXPECT_EQ(output.raw_report, report);
|
EXPECT_EQ(output.raw_report, report);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(ReportTest, ParsesSwitchProPlayerLightsAlongsideRumble) {
|
||||||
|
const auto profile = lvh::profiles::switch_pro();
|
||||||
|
const std::vector<std::uint8_t> report {
|
||||||
|
0x01,
|
||||||
|
0x07,
|
||||||
|
0x00,
|
||||||
|
0x01,
|
||||||
|
0x40,
|
||||||
|
0x40,
|
||||||
|
0x00,
|
||||||
|
0x01,
|
||||||
|
0x40,
|
||||||
|
0x40,
|
||||||
|
0x30,
|
||||||
|
0xA5,
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto outputs = lvh::reports::parse_output_reports(profile, report);
|
||||||
|
|
||||||
|
ASSERT_EQ(outputs.size(), 2U);
|
||||||
|
EXPECT_EQ(outputs[0].kind, lvh::GamepadOutputKind::rumble);
|
||||||
|
EXPECT_EQ(outputs[0].low_frequency_rumble, 0U);
|
||||||
|
EXPECT_EQ(outputs[0].high_frequency_rumble, 0U);
|
||||||
|
EXPECT_EQ(outputs[1].kind, lvh::GamepadOutputKind::player_leds);
|
||||||
|
EXPECT_EQ(outputs[1].player_leds, (std::array {true, false, true, false}));
|
||||||
|
EXPECT_EQ(outputs[1].flashing_player_leds, (std::array {false, true, false, true}));
|
||||||
|
EXPECT_EQ(outputs[1].raw_report, report);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ReportTest, ParsesSwitchProPlayerLightsWithMalformedRumbleData) {
|
||||||
|
const auto profile = lvh::profiles::switch_pro();
|
||||||
|
const std::vector<std::uint8_t> report {
|
||||||
|
0x01,
|
||||||
|
0x00,
|
||||||
|
0x74,
|
||||||
|
0x1A,
|
||||||
|
0x3D,
|
||||||
|
0x20,
|
||||||
|
0x74,
|
||||||
|
0x1A,
|
||||||
|
0x3D,
|
||||||
|
0x59,
|
||||||
|
0x30,
|
||||||
|
0x03,
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto outputs = lvh::reports::parse_output_reports(profile, report);
|
||||||
|
|
||||||
|
ASSERT_EQ(outputs.size(), 1U);
|
||||||
|
EXPECT_EQ(outputs[0].kind, lvh::GamepadOutputKind::player_leds);
|
||||||
|
EXPECT_EQ(outputs[0].player_leds, (std::array {true, true, false, false}));
|
||||||
|
EXPECT_EQ(outputs[0].flashing_player_leds, (std::array {false, false, false, false}));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ReportTest, ParsesSwitchProHomeLightAlongsideRumble) {
|
||||||
|
const auto profile = lvh::profiles::switch_pro();
|
||||||
|
const std::vector<std::uint8_t> report {
|
||||||
|
0x01,
|
||||||
|
0x09,
|
||||||
|
0x00,
|
||||||
|
0x01,
|
||||||
|
0x40,
|
||||||
|
0x40,
|
||||||
|
0x00,
|
||||||
|
0x01,
|
||||||
|
0x40,
|
||||||
|
0x40,
|
||||||
|
0x38,
|
||||||
|
0x01,
|
||||||
|
0xF0,
|
||||||
|
0xF0,
|
||||||
|
0x00,
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto outputs = lvh::reports::parse_output_reports(profile, report);
|
||||||
|
|
||||||
|
ASSERT_EQ(outputs.size(), 2U);
|
||||||
|
EXPECT_EQ(outputs[0].kind, lvh::GamepadOutputKind::rumble);
|
||||||
|
EXPECT_EQ(outputs[1].kind, lvh::GamepadOutputKind::rgb_led);
|
||||||
|
EXPECT_EQ(outputs[1].red, 255U);
|
||||||
|
EXPECT_EQ(outputs[1].green, 255U);
|
||||||
|
EXPECT_EQ(outputs[1].blue, 255U);
|
||||||
|
EXPECT_EQ(outputs[1].raw_report, report);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ReportTest, ParsesSwitchProHomeLightWithMalformedRumbleData) {
|
||||||
|
const auto profile = lvh::profiles::switch_pro();
|
||||||
|
const std::vector<std::uint8_t> report {
|
||||||
|
0x01,
|
||||||
|
0x00,
|
||||||
|
0x74,
|
||||||
|
0x1A,
|
||||||
|
0x3D,
|
||||||
|
0x20,
|
||||||
|
0x74,
|
||||||
|
0x1A,
|
||||||
|
0x3D,
|
||||||
|
0x59,
|
||||||
|
0x38,
|
||||||
|
0x01,
|
||||||
|
0x30,
|
||||||
|
0x30,
|
||||||
|
0x00,
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto outputs = lvh::reports::parse_output_reports(profile, report);
|
||||||
|
|
||||||
|
ASSERT_EQ(outputs.size(), 1U);
|
||||||
|
EXPECT_EQ(outputs[0].kind, lvh::GamepadOutputKind::rgb_led);
|
||||||
|
EXPECT_EQ(outputs[0].red, 77U);
|
||||||
|
EXPECT_EQ(outputs[0].green, 77U);
|
||||||
|
EXPECT_EQ(outputs[0].blue, 77U);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ReportTest, ParsesSwitchProHomeLightIntensityCurve) {
|
||||||
|
const auto profile = lvh::profiles::switch_pro();
|
||||||
|
std::vector<std::uint8_t> report {
|
||||||
|
0x01,
|
||||||
|
0x00,
|
||||||
|
0x74,
|
||||||
|
0x1A,
|
||||||
|
0x3D,
|
||||||
|
0x20,
|
||||||
|
0x74,
|
||||||
|
0x1A,
|
||||||
|
0x3D,
|
||||||
|
0x59,
|
||||||
|
0x38,
|
||||||
|
0x01,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
};
|
||||||
|
const std::array intensity_cases {
|
||||||
|
std::array<std::uint8_t, 2> {0x00, 0},
|
||||||
|
std::array<std::uint8_t, 2> {0x80, 184},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto &[encoded_intensity, expected_intensity] : intensity_cases) {
|
||||||
|
report[12] = encoded_intensity;
|
||||||
|
report[13] = encoded_intensity;
|
||||||
|
|
||||||
|
const auto outputs = lvh::reports::parse_output_reports(profile, report);
|
||||||
|
|
||||||
|
ASSERT_EQ(outputs.size(), 1U);
|
||||||
|
EXPECT_EQ(outputs[0].kind, lvh::GamepadOutputKind::rgb_led);
|
||||||
|
EXPECT_EQ(outputs[0].red, expected_intensity);
|
||||||
|
EXPECT_EQ(outputs[0].green, expected_intensity);
|
||||||
|
EXPECT_EQ(outputs[0].blue, expected_intensity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST(ReportTest, ParsesSwitchProNeutralRumbleReport) {
|
TEST(ReportTest, ParsesSwitchProNeutralRumbleReport) {
|
||||||
const auto profile = lvh::profiles::switch_pro();
|
const auto profile = lvh::profiles::switch_pro();
|
||||||
const std::vector<std::uint8_t> report {
|
const std::vector<std::uint8_t> report {
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ TEST(VirtualHidControlModelTest, NamesKnownAndFallbackEnumValues) {
|
|||||||
EXPECT_EQ(control::output_kind_name(lvh::GamepadOutputKind::adaptive_triggers), L"adaptive triggers");
|
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::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::trigger_rumble), L"trigger rumble");
|
||||||
|
EXPECT_EQ(control::output_kind_name(lvh::GamepadOutputKind::player_leds), L"player leds");
|
||||||
EXPECT_EQ(control::output_kind_name(static_cast<lvh::GamepadOutputKind>(255)), L"raw report");
|
EXPECT_EQ(control::output_kind_name(static_cast<lvh::GamepadOutputKind>(255)), L"raw report");
|
||||||
|
|
||||||
EXPECT_EQ(control::battery_state_name(lvh::GamepadBatteryState::unknown), L"unknown");
|
EXPECT_EQ(control::battery_state_name(lvh::GamepadBatteryState::unknown), L"unknown");
|
||||||
@@ -191,11 +192,11 @@ TEST(VirtualHidControlModelTest, SummarizesProfileFeatures) {
|
|||||||
|
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(
|
||||||
control::profile_feature_summary(generic),
|
control::profile_feature_summary(generic),
|
||||||
L"Features: battery no | rumble yes | trigger rumble no | RGB LED no | adaptive triggers no | raw output yes"
|
L"Features: battery no | rumble yes | trigger rumble no | RGB LED no | player LEDs no | adaptive triggers no | raw output yes"
|
||||||
);
|
);
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(
|
||||||
control::profile_feature_summary(dualsense),
|
control::profile_feature_summary(dualsense),
|
||||||
L"Features: battery yes | rumble yes | trigger rumble no | RGB LED yes | adaptive triggers yes | raw output yes"
|
L"Features: battery yes | rumble yes | trigger rumble no | RGB LED yes | player LEDs no | adaptive triggers yes | raw output yes"
|
||||||
);
|
);
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(
|
||||||
control::device_feature_summary(mouse),
|
control::device_feature_summary(mouse),
|
||||||
@@ -256,10 +257,13 @@ TEST(VirtualHidControlModelTest, SummarizesOutputState) {
|
|||||||
state.latest_rgb_led->blue = 3;
|
state.latest_rgb_led->blue = 3;
|
||||||
state.latest_adaptive_triggers = output(adaptive_triggers);
|
state.latest_adaptive_triggers = output(adaptive_triggers);
|
||||||
state.latest_adaptive_triggers->adaptive_trigger_flags = 4;
|
state.latest_adaptive_triggers->adaptive_trigger_flags = 4;
|
||||||
|
state.latest_player_leds = output(player_leds);
|
||||||
|
state.latest_player_leds->player_leds = {true, false, true, false};
|
||||||
|
state.latest_player_leds->flashing_player_leds = {false, true, false, true};
|
||||||
|
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(
|
||||||
control::output_summary(state, dualsense),
|
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 | adaptive flags=4 | player LEDs solid=1010 flashing=0101"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,14 +291,18 @@ TEST(VirtualHidControlModelTest, RecordsOutputsAndMaintainsLatestSummaryFields)
|
|||||||
adaptive.adaptive_trigger_flags = 8;
|
adaptive.adaptive_trigger_flags = 8;
|
||||||
control::record_output(state, adaptive, next_sequence, 3);
|
control::record_output(state, adaptive, next_sequence, 3);
|
||||||
|
|
||||||
|
auto player_leds = output(lvh::GamepadOutputKind::player_leds);
|
||||||
|
player_leds.player_leds = {true, false, true, false};
|
||||||
|
control::record_output(state, player_leds, next_sequence, 3);
|
||||||
|
|
||||||
auto raw = output(lvh::GamepadOutputKind::raw_report);
|
auto raw = output(lvh::GamepadOutputKind::raw_report);
|
||||||
raw.raw_report = {0x12, 0x34};
|
raw.raw_report = {0x12, 0x34};
|
||||||
control::record_output(state, raw, next_sequence, 3);
|
control::record_output(state, raw, next_sequence, 3);
|
||||||
|
|
||||||
ASSERT_EQ(state.outputs.size(), 3U);
|
ASSERT_EQ(state.outputs.size(), 3U);
|
||||||
EXPECT_EQ(state.outputs.front().sequence, 9U);
|
EXPECT_EQ(state.outputs.front().sequence, 10U);
|
||||||
EXPECT_EQ(state.outputs.back().sequence, 11U);
|
EXPECT_EQ(state.outputs.back().sequence, 12U);
|
||||||
EXPECT_EQ(next_sequence, 12U);
|
EXPECT_EQ(next_sequence, 13U);
|
||||||
|
|
||||||
ASSERT_TRUE(state.latest_rumble.has_value());
|
ASSERT_TRUE(state.latest_rumble.has_value());
|
||||||
EXPECT_EQ(state.latest_rumble->low_frequency_rumble, 100);
|
EXPECT_EQ(state.latest_rumble->low_frequency_rumble, 100);
|
||||||
@@ -304,6 +312,8 @@ TEST(VirtualHidControlModelTest, RecordsOutputsAndMaintainsLatestSummaryFields)
|
|||||||
EXPECT_EQ(state.latest_rgb_led->blue, 7);
|
EXPECT_EQ(state.latest_rgb_led->blue, 7);
|
||||||
ASSERT_TRUE(state.latest_adaptive_triggers.has_value());
|
ASSERT_TRUE(state.latest_adaptive_triggers.has_value());
|
||||||
EXPECT_EQ(state.latest_adaptive_triggers->adaptive_trigger_flags, 8);
|
EXPECT_EQ(state.latest_adaptive_triggers->adaptive_trigger_flags, 8);
|
||||||
|
ASSERT_TRUE(state.latest_player_leds.has_value());
|
||||||
|
EXPECT_EQ(state.latest_player_leds->player_leds, (std::array {true, false, true, false}));
|
||||||
ASSERT_TRUE(state.latest_raw_report.has_value());
|
ASSERT_TRUE(state.latest_raw_report.has_value());
|
||||||
EXPECT_EQ(state.latest_raw_report->raw_report, (std::vector<std::uint8_t> {0x12, 0x34}));
|
EXPECT_EQ(state.latest_raw_report->raw_report, (std::vector<std::uint8_t> {0x12, 0x34}));
|
||||||
}
|
}
|
||||||
@@ -105,6 +105,19 @@ TEST_F(WindowsBackendTest, FakeChannelExercisesLifecycleSubmitCloseAndOutput) {
|
|||||||
EXPECT_EQ(result.last_output.raw_report[0], 0x03U);
|
EXPECT_EQ(result.last_output.raw_report[0], 0x03U);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(WindowsBackendTest, SwitchReportsStreamAtTheNativeCadence) {
|
||||||
|
const auto result = lvh::detail::test::windows_backend_switch_report_stream();
|
||||||
|
|
||||||
|
expect_ok(result.create_status);
|
||||||
|
expect_ok(result.submit_status);
|
||||||
|
ASSERT_TRUE(result.repeated_report);
|
||||||
|
expect_ok(result.close_status);
|
||||||
|
ASSERT_GE(result.submitted_reports.size(), 2U);
|
||||||
|
EXPECT_TRUE(std::ranges::all_of(result.submitted_reports, [&result](const auto &report) {
|
||||||
|
return report == result.expected_report;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(WindowsBackendTest, PlayStationDefaultsUseEffectiveUsbProfiles) {
|
TEST_F(WindowsBackendTest, PlayStationDefaultsUseEffectiveUsbProfiles) {
|
||||||
const auto result = lvh::detail::test::windows_backend_playstation_transport();
|
const auto result = lvh::detail::test::windows_backend_playstation_transport();
|
||||||
const auto dualshock4_usb = lvh::profiles::dualshock4_usb();
|
const auto dualshock4_usb = lvh::profiles::dualshock4_usb();
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
@@ -59,6 +60,7 @@ namespace {
|
|||||||
std::wstring path;
|
std::wstring path;
|
||||||
std::uint16_t vendor_id = 0;
|
std::uint16_t vendor_id = 0;
|
||||||
std::uint16_t product_id = 0;
|
std::uint16_t product_id = 0;
|
||||||
|
std::uint16_t version_number = 0;
|
||||||
std::uint16_t usage = 0;
|
std::uint16_t usage = 0;
|
||||||
std::uint16_t input_report_size = 0;
|
std::uint16_t input_report_size = 0;
|
||||||
std::uint16_t output_report_size = 0;
|
std::uint16_t output_report_size = 0;
|
||||||
@@ -256,6 +258,7 @@ namespace {
|
|||||||
.path = detail->DevicePath,
|
.path = detail->DevicePath,
|
||||||
.vendor_id = attributes.VendorID,
|
.vendor_id = attributes.VendorID,
|
||||||
.product_id = attributes.ProductID,
|
.product_id = attributes.ProductID,
|
||||||
|
.version_number = attributes.VersionNumber,
|
||||||
.usage = capabilities.Usage,
|
.usage = capabilities.Usage,
|
||||||
.input_report_size = capabilities.InputReportByteLength,
|
.input_report_size = capabilities.InputReportByteLength,
|
||||||
.output_report_size = capabilities.OutputReportByteLength,
|
.output_report_size = capabilities.OutputReportByteLength,
|
||||||
@@ -317,6 +320,59 @@ namespace {
|
|||||||
return report;
|
return report;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template<typename Predicate>
|
||||||
|
std::optional<std::vector<std::uint8_t>> read_hid_report_matching(
|
||||||
|
HANDLE hid,
|
||||||
|
std::size_t report_size,
|
||||||
|
std::chrono::milliseconds timeout,
|
||||||
|
Predicate predicate
|
||||||
|
) {
|
||||||
|
const auto deadline = std::chrono::steady_clock::now() + timeout;
|
||||||
|
while (std::chrono::steady_clock::now() < deadline) {
|
||||||
|
const auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
deadline - std::chrono::steady_clock::now()
|
||||||
|
);
|
||||||
|
auto report = read_hid_report_with_timeout(hid, report_size, std::max(remaining, 1ms));
|
||||||
|
if (!report.has_value()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
if (predicate(*report)) {
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::vector<std::uint8_t>> send_switch_proprietary_command(
|
||||||
|
const HidGamepadInterface &hid_interface,
|
||||||
|
HANDLE reader,
|
||||||
|
HANDLE writer,
|
||||||
|
std::uint8_t command
|
||||||
|
) {
|
||||||
|
std::vector<std::uint8_t> output(hid_interface.output_report_size, 0);
|
||||||
|
output[0] = 0x80;
|
||||||
|
output[1] = command;
|
||||||
|
DWORD bytes_written = 0;
|
||||||
|
EXPECT_TRUE(WriteFile(
|
||||||
|
writer,
|
||||||
|
output.data(),
|
||||||
|
static_cast<DWORD>(output.size()),
|
||||||
|
&bytes_written,
|
||||||
|
nullptr
|
||||||
|
)) << "Switch proprietary WriteFile failed: "
|
||||||
|
<< GetLastError();
|
||||||
|
EXPECT_EQ(bytes_written, output.size());
|
||||||
|
return read_hid_report_matching(
|
||||||
|
reader,
|
||||||
|
hid_interface.input_report_size,
|
||||||
|
5s,
|
||||||
|
[command](const auto &report) {
|
||||||
|
return report.size() >= 2U && report[0] == 0x81U && report[1] == command;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
HidInterfacePaths current_gamepad_interface_paths() {
|
HidInterfacePaths current_gamepad_interface_paths() {
|
||||||
HidInterfacePaths paths;
|
HidInterfacePaths paths;
|
||||||
for (const auto &hid_interface : enumerate_gamepad_interfaces()) {
|
for (const auto &hid_interface : enumerate_gamepad_interfaces()) {
|
||||||
@@ -355,6 +411,16 @@ namespace {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<lvh::GamepadOutput> wait_for_player_leds(
|
||||||
|
const std::array<bool, 4> &solid,
|
||||||
|
const std::array<bool, 4> &flashing
|
||||||
|
) {
|
||||||
|
return wait_for([&solid, &flashing](const lvh::GamepadOutput &output) {
|
||||||
|
return output.kind == lvh::GamepadOutputKind::player_leds && output.player_leds == solid &&
|
||||||
|
output.flashing_player_leds == flashing;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
template<typename Predicate>
|
template<typename Predicate>
|
||||||
std::optional<lvh::GamepadOutput> wait_for(Predicate matches) {
|
std::optional<lvh::GamepadOutput> wait_for(Predicate matches) {
|
||||||
@@ -450,6 +516,25 @@ namespace {
|
|||||||
}
|
}
|
||||||
return {nullptr, &SDL_CloseGamepad};
|
return {nullptr, &SDL_CloseGamepad};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool wait_for_switch_pro_motion(SDL_Gamepad *gamepad) {
|
||||||
|
std::array<float, 3> acceleration {};
|
||||||
|
std::array<float, 3> gyroscope {};
|
||||||
|
const auto deadline = std::chrono::steady_clock::now() + 5s;
|
||||||
|
while (std::chrono::steady_clock::now() < deadline) {
|
||||||
|
SDL_UpdateGamepads();
|
||||||
|
SDL_PumpEvents();
|
||||||
|
if (
|
||||||
|
SDL_GetGamepadSensorData(gamepad, SDL_SENSOR_ACCEL, acceleration.data(), acceleration.size()) &&
|
||||||
|
SDL_GetGamepadSensorData(gamepad, SDL_SENSOR_GYRO, gyroscope.data(), gyroscope.size()) &&
|
||||||
|
std::abs(acceleration[1]) > 15.0F && std::abs(gyroscope[2]) > 0.25F
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
std::this_thread::sleep_for(20ms);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -505,6 +590,30 @@ TEST_F(WindowsConsumerTest, SdlHidapiOutputReachesDefaultPlayStationAndSwitchCal
|
|||||||
const auto rgb_led = output_capture.wait_for_rgb_led(0x12U, 0x34U, 0x56U);
|
const auto rgb_led = output_capture.wait_for_rgb_led(0x12U, 0x34U, 0x56U);
|
||||||
ASSERT_TRUE(rgb_led.has_value()) << "No normalized RGB LED callback followed SDL HIDAPI LED output";
|
ASSERT_TRUE(rgb_led.has_value()) << "No normalized RGB LED callback followed SDL HIDAPI LED output";
|
||||||
}
|
}
|
||||||
|
if (profile.gamepad_kind == lvh::GamepadProfileKind::switch_pro) {
|
||||||
|
ASSERT_TRUE(SDL_GetBooleanProperty(properties, SDL_PROP_GAMEPAD_CAP_MONO_LED_BOOLEAN, false)) << SDL_GetError();
|
||||||
|
ASSERT_TRUE(SDL_SetGamepadLED(gamepad.get(), 0xFFU, 0xFFU, 0xFFU)) << SDL_GetError();
|
||||||
|
const auto home_led = output_capture.wait_for_rgb_led(0xFFU, 0xFFU, 0xFFU);
|
||||||
|
ASSERT_TRUE(home_led.has_value()) << "No normalized HOME LED callback followed SDL HIDAPI LED output";
|
||||||
|
|
||||||
|
ASSERT_TRUE(SDL_GamepadHasSensor(gamepad.get(), SDL_SENSOR_ACCEL)) << SDL_GetError();
|
||||||
|
ASSERT_TRUE(SDL_GamepadHasSensor(gamepad.get(), SDL_SENSOR_GYRO)) << SDL_GetError();
|
||||||
|
ASSERT_TRUE(SDL_SetGamepadSensorEnabled(gamepad.get(), SDL_SENSOR_ACCEL, true)) << SDL_GetError();
|
||||||
|
ASSERT_TRUE(SDL_SetGamepadSensorEnabled(gamepad.get(), SDL_SENSOR_GYRO, true)) << SDL_GetError();
|
||||||
|
ASSERT_TRUE(created.adapter->set_motion(
|
||||||
|
lvh::Vector3 {.x = 9.80665F, .y = 19.6133F, .z = -9.80665F},
|
||||||
|
lvh::Vector3 {.x = 10.0F, .y = 20.0F, .z = -30.0F}
|
||||||
|
)
|
||||||
|
.ok());
|
||||||
|
ASSERT_TRUE(wait_for_switch_pro_motion(gamepad.get())) << "SDL HIDAPI did not receive live Switch Pro motion";
|
||||||
|
|
||||||
|
ASSERT_TRUE(SDL_SetGamepadPlayerIndex(gamepad.get(), 2)) << SDL_GetError();
|
||||||
|
const auto player_leds = output_capture.wait_for_player_leds(
|
||||||
|
std::array {true, true, true, false},
|
||||||
|
std::array {false, false, false, false}
|
||||||
|
);
|
||||||
|
ASSERT_TRUE(player_leds.has_value()) << "No normalized player LED callback followed SDL HIDAPI player-index output";
|
||||||
|
}
|
||||||
ASSERT_TRUE(SDL_GetBooleanProperty(properties, SDL_PROP_GAMEPAD_CAP_RUMBLE_BOOLEAN, false)) << SDL_GetError();
|
ASSERT_TRUE(SDL_GetBooleanProperty(properties, SDL_PROP_GAMEPAD_CAP_RUMBLE_BOOLEAN, false)) << SDL_GetError();
|
||||||
ASSERT_TRUE(SDL_RumbleGamepad(gamepad.get(), 0x5678U, 0x1234U, 1000U)) << SDL_GetError();
|
ASSERT_TRUE(SDL_RumbleGamepad(gamepad.get(), 0x5678U, 0x1234U, 1000U)) << SDL_GetError();
|
||||||
|
|
||||||
@@ -677,12 +786,14 @@ TEST_F(WindowsConsumerTest, NativeSwitchHandshakeAndInputReportReachHidClient) {
|
|||||||
|
|
||||||
lvh::CreateGamepadOptions options;
|
lvh::CreateGamepadOptions options;
|
||||||
options.profile = profile;
|
options.profile = profile;
|
||||||
|
options.metadata.stable_id = "02:00:00:00:00:05";
|
||||||
auto created = lvh::GamepadStateAdapter::create(*runtime, options);
|
auto created = lvh::GamepadStateAdapter::create(*runtime, options);
|
||||||
ASSERT_TRUE(created) << created.status.message();
|
ASSERT_TRUE(created) << created.status.message();
|
||||||
GamepadOutputCapture output_capture;
|
GamepadOutputCapture output_capture;
|
||||||
output_capture.attach(*created.adapter);
|
output_capture.attach(*created.adapter);
|
||||||
const auto hid_interface = wait_for_new_interface(previous_paths, profile.vendor_id, profile.product_id);
|
const auto hid_interface = wait_for_new_interface(previous_paths, profile.vendor_id, profile.product_id);
|
||||||
ASSERT_TRUE(hid_interface.has_value()) << "The VHF Switch Pro HID interface was not enumerated";
|
ASSERT_TRUE(hid_interface.has_value()) << "The VHF Switch Pro HID interface was not enumerated";
|
||||||
|
ASSERT_EQ(hid_interface->version_number, profile.version);
|
||||||
ASSERT_EQ(hid_interface->input_report_size, profile.input_report_size);
|
ASSERT_EQ(hid_interface->input_report_size, profile.input_report_size);
|
||||||
ASSERT_EQ(hid_interface->output_report_size, profile.output_report_size);
|
ASSERT_EQ(hid_interface->output_report_size, profile.output_report_size);
|
||||||
|
|
||||||
@@ -708,31 +819,14 @@ TEST_F(WindowsConsumerTest, NativeSwitchHandshakeAndInputReportReachHidClient) {
|
|||||||
)};
|
)};
|
||||||
ASSERT_TRUE(reader) << "Unable to open the VHF Switch Pro HID interface for reads: " << GetLastError();
|
ASSERT_TRUE(reader) << "Unable to open the VHF Switch Pro HID interface for reads: " << GetLastError();
|
||||||
|
|
||||||
const auto send_proprietary_command = [&hid_interface, &reader, &writer](std::uint8_t command) {
|
const auto status_reply = send_switch_proprietary_command(*hid_interface, reader.get(), writer.get(), 0x01);
|
||||||
std::vector<std::uint8_t> output(hid_interface->output_report_size, 0);
|
|
||||||
output[0] = 0x80;
|
|
||||||
output[1] = command;
|
|
||||||
DWORD bytes_written = 0;
|
|
||||||
EXPECT_TRUE(WriteFile(
|
|
||||||
writer.get(),
|
|
||||||
output.data(),
|
|
||||||
static_cast<DWORD>(output.size()),
|
|
||||||
&bytes_written,
|
|
||||||
nullptr
|
|
||||||
)) << "Switch proprietary WriteFile failed: "
|
|
||||||
<< GetLastError();
|
|
||||||
EXPECT_EQ(bytes_written, output.size());
|
|
||||||
return read_hid_report_with_timeout(reader.get(), hid_interface->input_report_size, 5s);
|
|
||||||
};
|
|
||||||
|
|
||||||
const auto status_reply = send_proprietary_command(0x01);
|
|
||||||
ASSERT_TRUE(status_reply.has_value()) << "No 0x81 Switch status reply reached the HID client";
|
ASSERT_TRUE(status_reply.has_value()) << "No 0x81 Switch status reply reached the HID client";
|
||||||
ASSERT_GE(status_reply->size(), 10U);
|
ASSERT_GE(status_reply->size(), 10U);
|
||||||
EXPECT_EQ(status_reply->at(0), 0x81U);
|
EXPECT_EQ(status_reply->at(0), 0x81U);
|
||||||
EXPECT_EQ(status_reply->at(1), 0x01U);
|
EXPECT_EQ(status_reply->at(1), 0x01U);
|
||||||
EXPECT_EQ(status_reply->at(3), 0x03U); // Pro Controller.
|
EXPECT_EQ(status_reply->at(3), 0x03U); // Pro Controller.
|
||||||
|
|
||||||
const auto handshake_reply = send_proprietary_command(0x02);
|
const auto handshake_reply = send_switch_proprietary_command(*hid_interface, reader.get(), writer.get(), 0x02);
|
||||||
ASSERT_TRUE(handshake_reply.has_value()) << "No 0x81 Switch handshake reply reached the HID client";
|
ASSERT_TRUE(handshake_reply.has_value()) << "No 0x81 Switch handshake reply reached the HID client";
|
||||||
ASSERT_GE(handshake_reply->size(), 2U);
|
ASSERT_GE(handshake_reply->size(), 2U);
|
||||||
EXPECT_EQ(handshake_reply->at(0), 0x81U);
|
EXPECT_EQ(handshake_reply->at(0), 0x81U);
|
||||||
@@ -769,6 +863,55 @@ TEST_F(WindowsConsumerTest, NativeSwitchHandshakeAndInputReportReachHidClient) {
|
|||||||
EXPECT_EQ(rumble->high_frequency_rumble, 5213U);
|
EXPECT_EQ(rumble->high_frequency_rumble, 5213U);
|
||||||
EXPECT_EQ(rumble->raw_report, rumble_report);
|
EXPECT_EQ(rumble->raw_report, rumble_report);
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> player_lights_report(hid_interface->output_report_size, 0);
|
||||||
|
player_lights_report[0] = 0x01;
|
||||||
|
player_lights_report[1] = 0x08;
|
||||||
|
constexpr std::array neutral_rumble {
|
||||||
|
std::uint8_t {0x00},
|
||||||
|
std::uint8_t {0x01},
|
||||||
|
std::uint8_t {0x40},
|
||||||
|
std::uint8_t {0x40},
|
||||||
|
std::uint8_t {0x00},
|
||||||
|
std::uint8_t {0x01},
|
||||||
|
std::uint8_t {0x40},
|
||||||
|
std::uint8_t {0x40},
|
||||||
|
};
|
||||||
|
std::ranges::copy(neutral_rumble, player_lights_report.begin() + 2);
|
||||||
|
player_lights_report[10] = 0x30;
|
||||||
|
player_lights_report[11] = 0xA5;
|
||||||
|
bytes_written = 0;
|
||||||
|
ASSERT_TRUE(WriteFile(
|
||||||
|
writer.get(),
|
||||||
|
player_lights_report.data(),
|
||||||
|
static_cast<DWORD>(player_lights_report.size()),
|
||||||
|
&bytes_written,
|
||||||
|
nullptr
|
||||||
|
)) << "Switch player-light WriteFile failed: "
|
||||||
|
<< GetLastError();
|
||||||
|
ASSERT_EQ(bytes_written, player_lights_report.size());
|
||||||
|
|
||||||
|
const auto player_lights_reply = read_hid_report_matching(
|
||||||
|
reader.get(),
|
||||||
|
hid_interface->input_report_size,
|
||||||
|
5s,
|
||||||
|
[](const auto &report) {
|
||||||
|
return report.size() >= 15U && report[0] == 0x21U && report[14] == 0x30U;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
ASSERT_TRUE(player_lights_reply.has_value()) << "No Switch player-light acknowledgement reached the HID client";
|
||||||
|
ASSERT_GE(player_lights_reply->size(), 15U);
|
||||||
|
EXPECT_EQ(player_lights_reply->at(0), 0x21U);
|
||||||
|
EXPECT_EQ(player_lights_reply->at(13), 0x80U);
|
||||||
|
EXPECT_EQ(player_lights_reply->at(14), 0x30U);
|
||||||
|
|
||||||
|
const auto player_lights = output_capture.wait_for_player_leds(
|
||||||
|
{true, false, true, false},
|
||||||
|
{false, true, false, true}
|
||||||
|
);
|
||||||
|
ASSERT_TRUE(player_lights.has_value())
|
||||||
|
<< "No normalized player-light callback followed the native Switch 0x30 output write";
|
||||||
|
EXPECT_EQ(player_lights->raw_report, player_lights_report);
|
||||||
|
|
||||||
lvh::GamepadState state;
|
lvh::GamepadState state;
|
||||||
state.buttons.set(lvh::GamepadButton::a);
|
state.buttons.set(lvh::GamepadButton::a);
|
||||||
state.buttons.set(lvh::GamepadButton::guide);
|
state.buttons.set(lvh::GamepadButton::guide);
|
||||||
@@ -776,9 +919,18 @@ TEST_F(WindowsConsumerTest, NativeSwitchHandshakeAndInputReportReachHidClient) {
|
|||||||
state.buttons.set(lvh::GamepadButton::right_stick);
|
state.buttons.set(lvh::GamepadButton::right_stick);
|
||||||
state.left_stick = {.x = 1.0F, .y = 0.0F};
|
state.left_stick = {.x = 1.0F, .y = 0.0F};
|
||||||
state.right_stick = {.x = 0.0F, .y = -1.0F};
|
state.right_stick = {.x = 0.0F, .y = -1.0F};
|
||||||
|
state.acceleration = lvh::Vector3 {.x = 9.80665F, .y = 19.6133F, .z = -9.80665F};
|
||||||
|
state.gyroscope = lvh::Vector3 {.x = 1.0F, .y = 2.0F, .z = -3.0F};
|
||||||
ASSERT_TRUE(created.adapter->set_state(state).ok());
|
ASSERT_TRUE(created.adapter->set_state(state).ok());
|
||||||
|
|
||||||
const auto input = read_hid_report_with_timeout(reader.get(), hid_interface->input_report_size, 5s);
|
const auto input = read_hid_report_matching(
|
||||||
|
reader.get(),
|
||||||
|
hid_interface->input_report_size,
|
||||||
|
5s,
|
||||||
|
[](const auto &report) {
|
||||||
|
return report.size() >= 6U && report[0] == 0x30U && report[3] == 0x08U && report[4] == 0x14U && report[5] == 0x08U;
|
||||||
|
}
|
||||||
|
);
|
||||||
ASSERT_TRUE(input.has_value()) << "No native Switch 0x30 input report reached the HID client";
|
ASSERT_TRUE(input.has_value()) << "No native Switch 0x30 input report reached the HID client";
|
||||||
ASSERT_EQ(input->size(), profile.input_report_size);
|
ASSERT_EQ(input->size(), profile.input_report_size);
|
||||||
EXPECT_EQ(input->at(0), 0x30U);
|
EXPECT_EQ(input->at(0), 0x30U);
|
||||||
@@ -791,6 +943,35 @@ TEST_F(WindowsConsumerTest, NativeSwitchHandshakeAndInputReportReachHidClient) {
|
|||||||
EXPECT_EQ(input->at(9), 0x00U);
|
EXPECT_EQ(input->at(9), 0x00U);
|
||||||
EXPECT_EQ(input->at(10), 0x08U);
|
EXPECT_EQ(input->at(10), 0x08U);
|
||||||
EXPECT_EQ(input->at(11), 0x00U);
|
EXPECT_EQ(input->at(11), 0x00U);
|
||||||
|
const auto read_i16 = [&input](std::size_t offset) {
|
||||||
|
return static_cast<std::int16_t>(
|
||||||
|
static_cast<std::uint16_t>(input->at(offset)) |
|
||||||
|
(static_cast<std::uint16_t>(input->at(offset + 1U)) << 8U)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
for (const auto offset : {13U, 25U, 37U}) {
|
||||||
|
EXPECT_EQ(read_i16(offset), 4096);
|
||||||
|
EXPECT_EQ(read_i16(offset + 2U), -4096);
|
||||||
|
EXPECT_EQ(read_i16(offset + 4U), 8192);
|
||||||
|
EXPECT_EQ(read_i16(offset + 6U), 43);
|
||||||
|
EXPECT_EQ(read_i16(offset + 8U), -14);
|
||||||
|
EXPECT_EQ(read_i16(offset + 10U), 29);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.gyroscope = lvh::Vector3 {.x = -4.0F, .y = 5.0F, .z = 6.0F};
|
||||||
|
ASSERT_TRUE(created.adapter->set_state(state).ok());
|
||||||
|
const auto next_input = read_hid_report_matching(
|
||||||
|
reader.get(),
|
||||||
|
hid_interface->input_report_size,
|
||||||
|
5s,
|
||||||
|
[&input](const auto &report) {
|
||||||
|
return report.size() > 19U && report[0] == 0x30U && report[19] != input->at(19);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
ASSERT_TRUE(next_input.has_value()) << "No second native Switch motion report reached the HID client";
|
||||||
|
ASSERT_EQ(next_input->size(), profile.input_report_size);
|
||||||
|
EXPECT_NE(next_input->at(1), input->at(1));
|
||||||
|
EXPECT_NE(next_input->at(19), input->at(19));
|
||||||
ASSERT_TRUE(created.adapter->close().ok());
|
ASSERT_TRUE(created.adapter->close().ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
// local includes
|
// local includes
|
||||||
#include "fixtures/fixtures.hpp"
|
#include "fixtures/fixtures.hpp"
|
||||||
#include "playstation_feature_protocol.hpp"
|
#include "playstation_feature_protocol.hpp"
|
||||||
#include "switch_pro_protocol.hpp"
|
#include "shared/switch_pro_protocol.hpp"
|
||||||
#include "windows_device_identity.hpp"
|
#include "windows_device_identity.hpp"
|
||||||
|
|
||||||
// standard includes
|
// standard includes
|
||||||
@@ -58,11 +58,11 @@ namespace {
|
|||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::array<std::uint8_t, lvh::detail::windows::switch_pro_report_size> switch_output_report(
|
std::array<std::uint8_t, lvh::detail::switch_pro_protocol::switch_pro_report_size> switch_output_report(
|
||||||
std::uint8_t report_id,
|
std::uint8_t report_id,
|
||||||
std::uint8_t command
|
std::uint8_t command
|
||||||
) {
|
) {
|
||||||
std::array<std::uint8_t, lvh::detail::windows::switch_pro_report_size> report {};
|
std::array<std::uint8_t, lvh::detail::switch_pro_protocol::switch_pro_report_size> report {};
|
||||||
report[0] = report_id;
|
report[0] = report_id;
|
||||||
if (report_id == 0x80U) {
|
if (report_id == 0x80U) {
|
||||||
report[1] = command;
|
report[1] = command;
|
||||||
@@ -116,7 +116,7 @@ TEST_F(WindowsDriverProtocolTest, NonSeriesXboxUsesItsPublicIdentityForMatching)
|
|||||||
|
|
||||||
TEST_F(WindowsDriverProtocolTest, SwitchProRepliesToUsbStatusAndHandshakeCommands) {
|
TEST_F(WindowsDriverProtocolTest, SwitchProRepliesToUsbStatusAndHandshakeCommands) {
|
||||||
auto status_report = switch_output_report(0x80, 0x01);
|
auto status_report = switch_output_report(0x80, 0x01);
|
||||||
const auto status_reply = lvh::detail::windows::make_switch_pro_reply(status_report);
|
const auto status_reply = lvh::detail::switch_pro_protocol::make_switch_pro_reply(status_report);
|
||||||
ASSERT_TRUE(status_reply.has_value());
|
ASSERT_TRUE(status_reply.has_value());
|
||||||
EXPECT_EQ(status_reply->at(0), 0x81);
|
EXPECT_EQ(status_reply->at(0), 0x81);
|
||||||
EXPECT_EQ(status_reply->at(1), 0x01);
|
EXPECT_EQ(status_reply->at(1), 0x01);
|
||||||
@@ -127,23 +127,23 @@ TEST_F(WindowsDriverProtocolTest, SwitchProRepliesToUsbStatusAndHandshakeCommand
|
|||||||
|
|
||||||
for (const auto command : {0x02U, 0x03U}) {
|
for (const auto command : {0x02U, 0x03U}) {
|
||||||
const auto report = switch_output_report(0x80, static_cast<std::uint8_t>(command));
|
const auto report = switch_output_report(0x80, static_cast<std::uint8_t>(command));
|
||||||
const auto reply = lvh::detail::windows::make_switch_pro_reply(report);
|
const auto reply = lvh::detail::switch_pro_protocol::make_switch_pro_reply(report);
|
||||||
ASSERT_TRUE(reply.has_value());
|
ASSERT_TRUE(reply.has_value());
|
||||||
EXPECT_EQ(reply->at(0), 0x81);
|
EXPECT_EQ(reply->at(0), 0x81);
|
||||||
EXPECT_EQ(reply->at(1), command);
|
EXPECT_EQ(reply->at(1), command);
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto force_usb = switch_output_report(0x80, 0x04);
|
const auto force_usb = switch_output_report(0x80, 0x04);
|
||||||
EXPECT_FALSE(lvh::detail::windows::make_switch_pro_reply(force_usb).has_value());
|
EXPECT_FALSE(lvh::detail::switch_pro_protocol::make_switch_pro_reply(force_usb).has_value());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(WindowsDriverProtocolTest, SwitchProAcknowledgesInitializationSubcommands) {
|
TEST_F(WindowsDriverProtocolTest, SwitchProAcknowledgesInitializationSubcommands) {
|
||||||
for (const auto subcommand : {0x03U, 0x30U, 0x38U, 0x40U, 0x41U, 0x48U}) {
|
for (const auto subcommand : {0x03U, 0x30U, 0x38U, 0x40U, 0x41U, 0x48U}) {
|
||||||
const auto report = switch_output_report(0x01, static_cast<std::uint8_t>(subcommand));
|
const auto report = switch_output_report(0x01, static_cast<std::uint8_t>(subcommand));
|
||||||
const auto reply = lvh::detail::windows::make_switch_pro_reply(report);
|
const auto reply = lvh::detail::switch_pro_protocol::make_switch_pro_reply(report, 0x42U);
|
||||||
ASSERT_TRUE(reply.has_value());
|
ASSERT_TRUE(reply.has_value());
|
||||||
EXPECT_EQ(reply->at(0), 0x21);
|
EXPECT_EQ(reply->at(0), 0x21);
|
||||||
EXPECT_EQ(reply->at(1), 0x07);
|
EXPECT_EQ(reply->at(1), 0x42);
|
||||||
EXPECT_EQ(reply->at(2), 0x81);
|
EXPECT_EQ(reply->at(2), 0x81);
|
||||||
EXPECT_EQ(reply->at(13), 0x80);
|
EXPECT_EQ(reply->at(13), 0x80);
|
||||||
EXPECT_EQ(reply->at(14), subcommand);
|
EXPECT_EQ(reply->at(14), subcommand);
|
||||||
@@ -156,12 +156,12 @@ TEST_F(WindowsDriverProtocolTest, SwitchProAcknowledgesInitializationSubcommands
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auto rumble_only = switch_output_report(0x10, 0x00);
|
const auto rumble_only = switch_output_report(0x10, 0x00);
|
||||||
EXPECT_FALSE(lvh::detail::windows::make_switch_pro_reply(rumble_only).has_value());
|
EXPECT_FALSE(lvh::detail::switch_pro_protocol::make_switch_pro_reply(rumble_only).has_value());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(WindowsDriverProtocolTest, SwitchProReturnsDeviceInfoAndFactoryCalibration) {
|
TEST_F(WindowsDriverProtocolTest, SwitchProReturnsDeviceInfoAndFactoryCalibration) {
|
||||||
auto device_info = switch_output_report(0x01, 0x02);
|
auto device_info = switch_output_report(0x01, 0x02);
|
||||||
const auto device_reply = lvh::detail::windows::make_switch_pro_reply(device_info);
|
const auto device_reply = lvh::detail::switch_pro_protocol::make_switch_pro_reply(device_info);
|
||||||
ASSERT_TRUE(device_reply.has_value());
|
ASSERT_TRUE(device_reply.has_value());
|
||||||
EXPECT_EQ(device_reply->at(13), 0x82);
|
EXPECT_EQ(device_reply->at(13), 0x82);
|
||||||
EXPECT_EQ(device_reply->at(14), 0x02);
|
EXPECT_EQ(device_reply->at(14), 0x02);
|
||||||
@@ -170,7 +170,7 @@ TEST_F(WindowsDriverProtocolTest, SwitchProReturnsDeviceInfoAndFactoryCalibratio
|
|||||||
auto stick_read = switch_output_report(0x01, 0x10);
|
auto stick_read = switch_output_report(0x01, 0x10);
|
||||||
write_u32(stick_read, 11U, 0x603D);
|
write_u32(stick_read, 11U, 0x603D);
|
||||||
stick_read[15] = 18U;
|
stick_read[15] = 18U;
|
||||||
const auto stick_reply = lvh::detail::windows::make_switch_pro_reply(stick_read);
|
const auto stick_reply = lvh::detail::switch_pro_protocol::make_switch_pro_reply(stick_read);
|
||||||
ASSERT_TRUE(stick_reply.has_value());
|
ASSERT_TRUE(stick_reply.has_value());
|
||||||
EXPECT_EQ(stick_reply->at(13), 0x90);
|
EXPECT_EQ(stick_reply->at(13), 0x90);
|
||||||
EXPECT_EQ(stick_reply->at(14), 0x10);
|
EXPECT_EQ(stick_reply->at(14), 0x10);
|
||||||
@@ -200,7 +200,7 @@ TEST_F(WindowsDriverProtocolTest, SwitchProReturnsDeviceInfoAndFactoryCalibratio
|
|||||||
auto imu_read = switch_output_report(0x01, 0x10);
|
auto imu_read = switch_output_report(0x01, 0x10);
|
||||||
write_u32(imu_read, 11U, 0x6020);
|
write_u32(imu_read, 11U, 0x6020);
|
||||||
imu_read[15] = 24U;
|
imu_read[15] = 24U;
|
||||||
const auto imu_reply = lvh::detail::windows::make_switch_pro_reply(imu_read);
|
const auto imu_reply = lvh::detail::switch_pro_protocol::make_switch_pro_reply(imu_read);
|
||||||
ASSERT_TRUE(imu_reply.has_value());
|
ASSERT_TRUE(imu_reply.has_value());
|
||||||
EXPECT_TRUE(std::ranges::any_of(imu_reply->begin() + 20, imu_reply->begin() + 44, [](auto value) {
|
EXPECT_TRUE(std::ranges::any_of(imu_reply->begin() + 20, imu_reply->begin() + 44, [](auto value) {
|
||||||
return value != 0U;
|
return value != 0U;
|
||||||
@@ -209,7 +209,7 @@ TEST_F(WindowsDriverProtocolTest, SwitchProReturnsDeviceInfoAndFactoryCalibratio
|
|||||||
auto user_read = switch_output_report(0x01, 0x10);
|
auto user_read = switch_output_report(0x01, 0x10);
|
||||||
write_u32(user_read, 11U, 0x8010);
|
write_u32(user_read, 11U, 0x8010);
|
||||||
user_read[15] = 22U;
|
user_read[15] = 22U;
|
||||||
const auto user_reply = lvh::detail::windows::make_switch_pro_reply(user_read);
|
const auto user_reply = lvh::detail::switch_pro_protocol::make_switch_pro_reply(user_read);
|
||||||
ASSERT_TRUE(user_reply.has_value());
|
ASSERT_TRUE(user_reply.has_value());
|
||||||
EXPECT_TRUE(std::ranges::all_of(user_reply->begin() + 20, user_reply->begin() + 42, [](auto value) {
|
EXPECT_TRUE(std::ranges::all_of(user_reply->begin() + 20, user_reply->begin() + 42, [](auto value) {
|
||||||
return value == 0U;
|
return value == 0U;
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ namespace lvh::tools::virtualhid_control {
|
|||||||
return L"raw report";
|
return L"raw report";
|
||||||
case trigger_rumble:
|
case trigger_rumble:
|
||||||
return L"trigger rumble";
|
return L"trigger rumble";
|
||||||
|
case player_leds:
|
||||||
|
return L"player leds";
|
||||||
}
|
}
|
||||||
return L"raw report";
|
return L"raw report";
|
||||||
}
|
}
|
||||||
@@ -223,7 +225,8 @@ namespace lvh::tools::virtualhid_control {
|
|||||||
return supports_gamepad_output(profile, rumble) ||
|
return supports_gamepad_output(profile, rumble) ||
|
||||||
supports_gamepad_output(profile, rgb_led) ||
|
supports_gamepad_output(profile, rgb_led) ||
|
||||||
supports_gamepad_output(profile, adaptive_triggers) ||
|
supports_gamepad_output(profile, adaptive_triggers) ||
|
||||||
supports_gamepad_output(profile, trigger_rumble);
|
supports_gamepad_output(profile, trigger_rumble) ||
|
||||||
|
supports_gamepad_output(profile, player_leds);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::wstring profile_feature_summary(const DeviceProfile &profile) {
|
std::wstring profile_feature_summary(const DeviceProfile &profile) {
|
||||||
@@ -235,6 +238,7 @@ namespace lvh::tools::virtualhid_control {
|
|||||||
stream << L" | rumble " << yes_no(supports_gamepad_output(profile, rumble));
|
stream << L" | rumble " << yes_no(supports_gamepad_output(profile, rumble));
|
||||||
stream << L" | trigger rumble " << yes_no(supports_gamepad_output(profile, trigger_rumble));
|
stream << L" | trigger rumble " << yes_no(supports_gamepad_output(profile, trigger_rumble));
|
||||||
stream << L" | RGB LED " << yes_no(supports_gamepad_output(profile, rgb_led));
|
stream << L" | RGB LED " << yes_no(supports_gamepad_output(profile, rgb_led));
|
||||||
|
stream << L" | player LEDs " << yes_no(supports_gamepad_output(profile, player_leds));
|
||||||
stream << L" | adaptive triggers " << yes_no(supports_gamepad_output(profile, adaptive_triggers));
|
stream << L" | adaptive triggers " << yes_no(supports_gamepad_output(profile, adaptive_triggers));
|
||||||
stream << L" | raw output " << yes_no(supports_gamepad_output(profile, raw_report));
|
stream << L" | raw output " << yes_no(supports_gamepad_output(profile, raw_report));
|
||||||
return stream.str();
|
return stream.str();
|
||||||
@@ -272,6 +276,18 @@ namespace lvh::tools::virtualhid_control {
|
|||||||
stream << L"adaptive flags=" << static_cast<unsigned>(state.latest_adaptive_triggers->adaptive_trigger_flags);
|
stream << L"adaptive flags=" << static_cast<unsigned>(state.latest_adaptive_triggers->adaptive_trigger_flags);
|
||||||
wrote = true;
|
wrote = true;
|
||||||
}
|
}
|
||||||
|
if (state.latest_player_leds) {
|
||||||
|
append_summary_separator(stream, wrote);
|
||||||
|
stream << L"player LEDs solid=";
|
||||||
|
for (const auto active : state.latest_player_leds->player_leds) {
|
||||||
|
stream << (active ? L'1' : L'0');
|
||||||
|
}
|
||||||
|
stream << L" flashing=";
|
||||||
|
for (const auto active : state.latest_player_leds->flashing_player_leds) {
|
||||||
|
stream << (active ? L'1' : L'0');
|
||||||
|
}
|
||||||
|
wrote = true;
|
||||||
|
}
|
||||||
if (!wrote && state.latest_raw_report) {
|
if (!wrote && state.latest_raw_report) {
|
||||||
stream << L"raw report";
|
stream << L"raw report";
|
||||||
wrote = true;
|
wrote = true;
|
||||||
@@ -339,6 +355,9 @@ namespace lvh::tools::virtualhid_control {
|
|||||||
case adaptive_triggers:
|
case adaptive_triggers:
|
||||||
state.latest_adaptive_triggers = output;
|
state.latest_adaptive_triggers = output;
|
||||||
break;
|
break;
|
||||||
|
case player_leds:
|
||||||
|
state.latest_player_leds = output;
|
||||||
|
break;
|
||||||
case raw_report:
|
case raw_report:
|
||||||
state.latest_raw_report = output;
|
state.latest_raw_report = output;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ namespace lvh::tools::virtualhid_control {
|
|||||||
std::optional<GamepadOutput> latest_trigger_rumble;
|
std::optional<GamepadOutput> latest_trigger_rumble;
|
||||||
std::optional<GamepadOutput> latest_rgb_led;
|
std::optional<GamepadOutput> latest_rgb_led;
|
||||||
std::optional<GamepadOutput> latest_adaptive_triggers;
|
std::optional<GamepadOutput> latest_adaptive_triggers;
|
||||||
|
std::optional<GamepadOutput> latest_player_leds;
|
||||||
std::optional<GamepadOutput> latest_raw_report;
|
std::optional<GamepadOutput> latest_raw_report;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user